import UBI rsync-3.2.5-7.el9_8.2
This commit is contained in:
parent
fdc9f82f8e
commit
eccd20cd37
@ -1,593 +0,0 @@
|
||||
From b4a27ca25d0abb6fcf14f41b7e11f3a6e1d8a4ff Mon Sep 17 00:00:00 2001
|
||||
From: Andrew Tridgell <andrew@tridgell.net>
|
||||
Date: Sat, 23 Nov 2024 12:26:10 +1100
|
||||
Subject: [PATCH] added secure_relative_open()
|
||||
|
||||
this is an open that enforces no symlink following for all path
|
||||
components in a relative path
|
||||
---
|
||||
syscall.c | 74 +++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
1 file changed, 74 insertions(+)
|
||||
|
||||
diff --git a/syscall.c b/syscall.c
|
||||
index b4b0f1f16..cffc814b7 100644
|
||||
--- a/syscall.c
|
||||
+++ b/syscall.c
|
||||
@@ -33,6 +33,8 @@
|
||||
#include <sys/syscall.h>
|
||||
#endif
|
||||
|
||||
+#include "ifuncs.h"
|
||||
+
|
||||
extern int dry_run;
|
||||
extern int am_root;
|
||||
extern int am_sender;
|
||||
diff --git a/syscall.c b/syscall.c
|
||||
index 1dafab7..c8391f4 100644
|
||||
--- a/syscall.c
|
||||
+++ b/syscall.c
|
||||
@@ -734,3 +734,75 @@ int do_open_checklinks(const char *pathname)
|
||||
}
|
||||
return do_open_nofollow(pathname, O_RDONLY);
|
||||
}
|
||||
+
|
||||
+/*
|
||||
+ open a file relative to a base directory. The basedir can be NULL,
|
||||
+ in which case the current working directory is used. The relpath
|
||||
+ must be a relative path, and the relpath must not contain any
|
||||
+ elements in the path which follow symlinks (ie. like O_NOFOLLOW, but
|
||||
+ applies to all path components, not just the last component)
|
||||
+*/
|
||||
+int secure_relative_open(const char *basedir, const char *relpath, int flags, mode_t mode)
|
||||
+{
|
||||
+ if (!relpath || relpath[0] == '/') {
|
||||
+ // must be a relative path
|
||||
+ errno = EINVAL;
|
||||
+ return -1;
|
||||
+ }
|
||||
+
|
||||
+#if !defined(O_NOFOLLOW) || !defined(O_DIRECTORY)
|
||||
+ // really old system, all we can do is live with the risks
|
||||
+ if (!basedir) {
|
||||
+ return open(relpath, flags, mode);
|
||||
+ }
|
||||
+ char fullpath[MAXPATHLEN];
|
||||
+ pathjoin(fullpath, sizeof fullpath, basedir, relpath);
|
||||
+ return open(fullpath, flags, mode);
|
||||
+#else
|
||||
+ int dirfd = AT_FDCWD;
|
||||
+ if (basedir != NULL) {
|
||||
+ dirfd = openat(AT_FDCWD, basedir, O_RDONLY | O_DIRECTORY);
|
||||
+ if (dirfd == -1) {
|
||||
+ return -1;
|
||||
+ }
|
||||
+ }
|
||||
+ int retfd = -1;
|
||||
+
|
||||
+ char *path_copy = my_strdup(relpath, __FILE__, __LINE__);
|
||||
+ if (!path_copy) {
|
||||
+ return -1;
|
||||
+ }
|
||||
+
|
||||
+ for (const char *part = strtok(path_copy, "/");
|
||||
+ part != NULL;
|
||||
+ part = strtok(NULL, "/"))
|
||||
+ {
|
||||
+ int next_fd = openat(dirfd, part, O_RDONLY | O_DIRECTORY | O_NOFOLLOW);
|
||||
+ if (next_fd == -1 && errno == ENOTDIR) {
|
||||
+ if (strtok(NULL, "/") != NULL) {
|
||||
+ // this is not the last component of the path
|
||||
+ errno = ELOOP;
|
||||
+ goto cleanup;
|
||||
+ }
|
||||
+ // this could be the last component of the path, try as a file
|
||||
+ retfd = openat(dirfd, part, flags | O_NOFOLLOW, mode);
|
||||
+ goto cleanup;
|
||||
+ }
|
||||
+ if (next_fd == -1) {
|
||||
+ goto cleanup;
|
||||
+ }
|
||||
+ if (dirfd != AT_FDCWD) close(dirfd);
|
||||
+ dirfd = next_fd;
|
||||
+ }
|
||||
+
|
||||
+ // the path must be a directory
|
||||
+ errno = EINVAL;
|
||||
+
|
||||
+cleanup:
|
||||
+ free(path_copy);
|
||||
+ if (dirfd != AT_FDCWD) {
|
||||
+ close(dirfd);
|
||||
+ }
|
||||
+ return retfd;
|
||||
+#endif // O_NOFOLLOW, O_DIRECTORY
|
||||
+}
|
||||
From c35e28331f10ba6eba370611abd78bde32d54da7 Mon Sep 17 00:00:00 2001
|
||||
From: Andrew Tridgell <andrew@tridgell.net>
|
||||
Date: Sat, 23 Nov 2024 12:28:13 +1100
|
||||
Subject: [PATCH] receiver: use secure_relative_open() for basis file
|
||||
|
||||
this prevents attacks where the basis file is manipulated by a
|
||||
malicious sender to gain information about files outside the
|
||||
destination tree
|
||||
---
|
||||
receiver.c | 42 ++++++++++++++++++++++++++----------------
|
||||
1 file changed, 26 insertions(+), 16 deletions(-)
|
||||
|
||||
diff --git a/receiver.c b/receiver.c
|
||||
index 2d7f60330..8031b8f4b 100644
|
||||
--- a/receiver.c
|
||||
+++ b/receiver.c
|
||||
@@ -552,6 +552,8 @@ int recv_files(int f_in, int f_out, char *local_name)
|
||||
progress_init();
|
||||
|
||||
while (1) {
|
||||
+ const char *basedir = NULL;
|
||||
+
|
||||
cleanup_disable();
|
||||
|
||||
/* This call also sets cur_flist. */
|
||||
@@ -719,27 +719,29 @@ int recv_files(int f_in, int f_out, char *local_name)
|
||||
break;
|
||||
case FNAMECMP_FUZZY:
|
||||
if (file->dirname) {
|
||||
- pathjoin(fnamecmpbuf, sizeof fnamecmpbuf, file->dirname, xname);
|
||||
- fnamecmp = fnamecmpbuf;
|
||||
- } else
|
||||
- fnamecmp = xname;
|
||||
+ basedir = file->dirname;
|
||||
+ }
|
||||
+ fnamecmp = xname;
|
||||
break;
|
||||
default:
|
||||
if (fnamecmp_type > FNAMECMP_FUZZY && fnamecmp_type-FNAMECMP_FUZZY <= basis_dir_cnt) {
|
||||
fnamecmp_type -= FNAMECMP_FUZZY + 1;
|
||||
if (file->dirname) {
|
||||
- stringjoin(fnamecmpbuf, sizeof fnamecmpbuf,
|
||||
- basis_dir[fnamecmp_type], "/", file->dirname, "/", xname, NULL);
|
||||
- } else
|
||||
- pathjoin(fnamecmpbuf, sizeof fnamecmpbuf, basis_dir[fnamecmp_type], xname);
|
||||
+ pathjoin(fnamecmpbuf, sizeof fnamecmpbuf, basis_dir[fnamecmp_type], file->dirname);
|
||||
+ basedir = fnamecmpbuf;
|
||||
+ } else {
|
||||
+ basedir = basis_dir[fnamecmp_type];
|
||||
+ }
|
||||
+ fnamecmp = xname;
|
||||
} else if (fnamecmp_type >= basis_dir_cnt) {
|
||||
rprintf(FERROR,
|
||||
"invalid basis_dir index: %d.\n",
|
||||
fnamecmp_type);
|
||||
exit_cleanup(RERR_PROTOCOL);
|
||||
- } else
|
||||
- pathjoin(fnamecmpbuf, sizeof fnamecmpbuf, basis_dir[fnamecmp_type], fname);
|
||||
- fnamecmp = fnamecmpbuf;
|
||||
+ } else {
|
||||
+ basedir = basis_dir[fnamecmp_type];
|
||||
+ fnamecmp = fname;
|
||||
+ }
|
||||
break;
|
||||
}
|
||||
if (!fnamecmp || (daemon_filter_list.head
|
||||
@@ -765,7 +769,7 @@ int recv_files(int f_in, int f_out, char *local_name)
|
||||
}
|
||||
|
||||
/* open the file */
|
||||
- fd1 = do_open(fnamecmp, O_RDONLY, 0);
|
||||
+ fd1 = secure_relative_open(basedir, fnamecmp, O_RDONLY, 0);
|
||||
|
||||
if (fd1 == -1 && protocol_version < 29) {
|
||||
if (fnamecmp != fname) {
|
||||
@@ -776,14 +780,20 @@ int recv_files(int f_in, int f_out, char *local_name)
|
||||
|
||||
if (fd1 == -1 && basis_dir[0]) {
|
||||
/* pre-29 allowed only one alternate basis */
|
||||
- pathjoin(fnamecmpbuf, sizeof fnamecmpbuf,
|
||||
- basis_dir[0], fname);
|
||||
- fnamecmp = fnamecmpbuf;
|
||||
+ basedir = basis_dir[0];
|
||||
+ fnamecmp = fname;
|
||||
fnamecmp_type = FNAMECMP_BASIS_DIR_LOW;
|
||||
- fd1 = do_open(fnamecmp, O_RDONLY, 0);
|
||||
+ fd1 = secure_relative_open(basedir, fnamecmp, O_RDONLY, 0);
|
||||
}
|
||||
}
|
||||
|
||||
+ if (basedir) {
|
||||
+ // for the following code we need the full
|
||||
+ // path name as a single string
|
||||
+ pathjoin(fnamecmpbuf, sizeof fnamecmpbuf, basedir, fnamecmp);
|
||||
+ fnamecmp = fnamecmpbuf;
|
||||
+ }
|
||||
+
|
||||
one_inplace = inplace_partial && fnamecmp_type == FNAMECMP_PARTIAL_DIR;
|
||||
updating_basis_or_equiv = one_inplace
|
||||
|| (inplace && (fnamecmp == fname || fnamecmp_type == FNAMECMP_BACKUP));
|
||||
From 4fa7156ccdb2ad34b034d18fe2fd6cd79adef8a1 Mon Sep 17 00:00:00 2001
|
||||
From: Andrew Tridgell <andrew@tridgell.net>
|
||||
Date: Thu, 30 Apr 2026 08:39:22 +1000
|
||||
Subject: [PATCH] syscall: use openat2(RESOLVE_BENEATH) on Linux for
|
||||
secure_relative_open
|
||||
|
||||
The CVE fix in commit c35e283 made secure_relative_open() walk every
|
||||
component of relpath with O_NOFOLLOW. That blocks every symlink in the
|
||||
path, which is stricter than the threat model required: legitimate
|
||||
directory symlinks within the destination tree (e.g. when using -K /
|
||||
--copy-dirlinks) are also rejected, breaking delta transfers with
|
||||
"failed verification -- update discarded". See issue #715.
|
||||
|
||||
On Linux 5.6+, openat2(RESOLVE_BENEATH | RESOLVE_NO_MAGICLINKS) gives
|
||||
us exactly what we want: the kernel rejects any resolution that would
|
||||
escape the starting directory (via "..", absolute paths, or symlinks
|
||||
pointing outside dirfd) while still following symlinks that resolve
|
||||
within it. /proc magic-links are blocked too.
|
||||
|
||||
Use openat2 first; fall back to the existing per-component O_NOFOLLOW
|
||||
walk on ENOSYS (kernel < 5.6). The lexical "../" checks at the head
|
||||
of the function are kept as defense in depth. The Linux gate is
|
||||
plain #ifdef __linux__: the runtime ENOSYS fallback covers the only
|
||||
case that actually matters (header present + old kernel), and any
|
||||
Linux build environment without linux/openat2.h will fail with a
|
||||
clear "no such file" error rather than silently disabling the
|
||||
protection.
|
||||
|
||||
Verified manually that openat2(RESOLVE_BENEATH) blocks all four
|
||||
escape patterns (absolute symlink, ../ symlink, lexical .., absolute
|
||||
path) while allowing direct and within-tree symlinks. The new
|
||||
testsuite/symlink-dirlink-basis.test (taken from PR #864 by Samuel
|
||||
Henrique) exercises the issue #715 regression and passes; full
|
||||
make check passes 47/47.
|
||||
|
||||
Test: testsuite/symlink-dirlink-basis.test (8 scenarios)
|
||||
Fixes: https://github.com/RsyncProject/rsync/issues/715
|
||||
|
||||
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||||
---
|
||||
syscall.c | 62 ++++++-
|
||||
testsuite/symlink-dirlink-basis.test | 247 +++++++++++++++++++++++++++
|
||||
2 files changed, 304 insertions(+), 5 deletions(-)
|
||||
create mode 100755 testsuite/symlink-dirlink-basis.test
|
||||
|
||||
diff --git a/syscall.c b/syscall.c
|
||||
index ec0e0708a..66c6d29c7 100644
|
||||
--- a/syscall.c
|
||||
+++ b/syscall.c
|
||||
@@ -33,6 +33,11 @@
|
||||
#include <sys/syscall.h>
|
||||
#endif
|
||||
|
||||
+#ifdef __linux__
|
||||
+#include <sys/syscall.h>
|
||||
+#include <linux/openat2.h>
|
||||
+#endif
|
||||
+
|
||||
#include "ifuncs.h"
|
||||
|
||||
extern int dry_run;
|
||||
@@ -743,10 +743,49 @@ int do_open_checklinks(const char *pathname)
|
||||
/*
|
||||
open a file relative to a base directory. The basedir can be NULL,
|
||||
in which case the current working directory is used. The relpath
|
||||
- must be a relative path, and the relpath must not contain any
|
||||
- elements in the path which follow symlinks (ie. like O_NOFOLLOW, but
|
||||
- applies to all path components, not just the last component)
|
||||
+ must be a relative path. The kernel must guarantee that resolution
|
||||
+ cannot escape basedir (or the cwd, when basedir is NULL): no ".."
|
||||
+ jumps above the start, no symlinks pointing outside, no absolute
|
||||
+ paths, no /proc magic-link tricks.
|
||||
+
|
||||
+ Symlinks *within* basedir are followed normally — earlier rsync
|
||||
+ versions rejected every symlink with O_NOFOLLOW on each component,
|
||||
+ which broke legitimate directory symlinks on the receiver side
|
||||
+ (https://github.com/RsyncProject/rsync/issues/715). The escape
|
||||
+ prevention is handled by the kernel via openat2(RESOLVE_BENEATH)
|
||||
+ on Linux 5.6+; older systems fall back to the per-component
|
||||
+ O_NOFOLLOW walk below.
|
||||
+
|
||||
+ The relpath must also not contain any ../ elements in the path.
|
||||
*/
|
||||
+
|
||||
+#ifdef __linux__
|
||||
+static int secure_relative_open_linux(const char *basedir, const char *relpath, int flags, mode_t mode)
|
||||
+{
|
||||
+ struct open_how how;
|
||||
+ int dirfd, retfd;
|
||||
+
|
||||
+ memset(&how, 0, sizeof how);
|
||||
+ how.flags = flags;
|
||||
+ how.mode = mode;
|
||||
+ how.resolve = RESOLVE_BENEATH | RESOLVE_NO_MAGICLINKS;
|
||||
+
|
||||
+ if (basedir == NULL) {
|
||||
+ dirfd = AT_FDCWD;
|
||||
+ } else {
|
||||
+ dirfd = openat(AT_FDCWD, basedir, O_RDONLY | O_DIRECTORY);
|
||||
+ if (dirfd == -1)
|
||||
+ return -1;
|
||||
+ }
|
||||
+
|
||||
+ retfd = syscall(SYS_openat2, dirfd, relpath, &how, sizeof how);
|
||||
+
|
||||
+ if (dirfd != AT_FDCWD)
|
||||
+ close(dirfd);
|
||||
+ return retfd;
|
||||
+}
|
||||
+#endif
|
||||
+
|
||||
int secure_relative_open(const char *basedir, const char *relpath, int flags, mode_t mode)
|
||||
{
|
||||
if (!relpath || relpath[0] == '/') {
|
||||
@@ -754,6 +793,21 @@ int secure_relative_open(const char *basedir, const char *relpath, int flags, mo
|
||||
errno = EINVAL;
|
||||
return -1;
|
||||
}
|
||||
+ if (strncmp(relpath, "../", 3) == 0 || strstr(relpath, "/../")) {
|
||||
+ // no ../ elements allowed in the relpath
|
||||
+ errno = EINVAL;
|
||||
+ return -1;
|
||||
+ }
|
||||
+
|
||||
+#ifdef __linux__
|
||||
+ {
|
||||
+ int fd = secure_relative_open_linux(basedir, relpath, flags, mode);
|
||||
+ /* ENOSYS = kernel < 5.6 doesn't have the syscall even though
|
||||
+ * glibc/kernel-headers do; fall through to the portable path. */
|
||||
+ if (fd != -1 || errno != ENOSYS)
|
||||
+ return fd;
|
||||
+ }
|
||||
+#endif
|
||||
|
||||
#if !defined(O_NOFOLLOW) || !defined(O_DIRECTORY)
|
||||
// really old system, all we can do is live with the risks
|
||||
diff --git a/testsuite/symlink-dirlink-basis.test b/testsuite/symlink-dirlink-basis.test
|
||||
new file mode 100755
|
||||
index 000000000..9065dd814
|
||||
--- /dev/null
|
||||
+++ b/testsuite/symlink-dirlink-basis.test
|
||||
@@ -0,0 +1,247 @@
|
||||
+#!/bin/sh
|
||||
+
|
||||
+# Test that updating a file through a directory symlink works when using
|
||||
+# -K (--copy-dirlinks). This is a regression test for:
|
||||
+# https://github.com/RsyncProject/rsync/issues/715
|
||||
+#
|
||||
+# The CVE fix in commit c35e283 introduced secure_relative_open() which
|
||||
+# uses O_NOFOLLOW on all path components, breaking legitimate directory
|
||||
+# symlinks on the receiver side. The fix splits the path into basedir
|
||||
+# (dirname, symlinks followed) and basename (O_NOFOLLOW) so that
|
||||
+# directory symlinks are traversed while the final file component is
|
||||
+# still protected.
|
||||
+#
|
||||
+# The regression only manifests when delta matching is triggered (i.e.,
|
||||
+# the sender finds matching blocks in the old file). Small files with
|
||||
+# completely different content are transferred in full and don't trigger
|
||||
+# the bug. We use a large file with a small modification to ensure
|
||||
+# delta transfer is used.
|
||||
+#
|
||||
+# In addition to the original regression, this test covers edge cases
|
||||
+# in the fix itself:
|
||||
+# - --backup with directory symlinks (finish_transfer pointer identity)
|
||||
+# - --partial-dir with protocol < 29 (fnamecmp != partialptr guard)
|
||||
+# - --inplace with directory symlinks (updating_basis_or_equiv check)
|
||||
+# - Files without a dirname (top-level files, no split needed)
|
||||
+
|
||||
+. "$suitedir/rsync.fns"
|
||||
+
|
||||
+RSYNC_RSH="$scratchdir/src/support/lsh.sh"
|
||||
+export RSYNC_RSH
|
||||
+
|
||||
+# $HOME is set to $scratchdir by rsync.fns
|
||||
+# localhost: destination will cd to $HOME (i.e., $scratchdir)
|
||||
+
|
||||
+# Helper: create a large file suitable for delta transfers.
|
||||
+# ~32KB is large enough for rsync's block matching to find matches.
|
||||
+make_testfile() {
|
||||
+ dd if=/dev/urandom of="$1" bs=1024 count=32 2>/dev/null \
|
||||
+ || test_fail "failed to create test file $1"
|
||||
+}
|
||||
+
|
||||
+# Set up source tree
|
||||
+srcbase="$tmpdir/src"
|
||||
+
|
||||
+######################################################################
|
||||
+# Test 1: Basic directory symlink update (the original issue #715)
|
||||
+######################################################################
|
||||
+
|
||||
+mkdir -p "$HOME/real-dir"
|
||||
+ln -s real-dir "$HOME/dir"
|
||||
+
|
||||
+mkdir -p "$srcbase/dir"
|
||||
+make_testfile "$srcbase/dir/file"
|
||||
+
|
||||
+# First transfer (initial): should create the file through the symlink
|
||||
+(cd "$srcbase" && $RSYNC -KRlptv --rsync-path="$RSYNC" dir/file localhost:) \
|
||||
+ || test_fail "test 1: initial transfer failed"
|
||||
+
|
||||
+if [ ! -f "$HOME/real-dir/file" ]; then
|
||||
+ test_fail "test 1: initial transfer did not create file through symlink"
|
||||
+fi
|
||||
+
|
||||
+diff "$srcbase/dir/file" "$HOME/real-dir/file" >/dev/null \
|
||||
+ || test_fail "test 1: initial transfer content mismatch"
|
||||
+
|
||||
+# Small modification to trigger delta transfer
|
||||
+echo "appended update" >> "$srcbase/dir/file"
|
||||
+sleep 1
|
||||
+touch "$srcbase/dir/file"
|
||||
+
|
||||
+# Second transfer (update): was failing with "failed verification"
|
||||
+(cd "$srcbase" && $RSYNC -KRlptv --rsync-path="$RSYNC" dir/file localhost:) \
|
||||
+ || test_fail "test 1: update through directory symlink failed"
|
||||
+
|
||||
+diff "$srcbase/dir/file" "$HOME/real-dir/file" >/dev/null \
|
||||
+ || test_fail "test 1: update transfer content mismatch"
|
||||
+
|
||||
+######################################################################
|
||||
+# Test 2: Compression (-z) as in the original reproducer
|
||||
+######################################################################
|
||||
+
|
||||
+echo "another line" >> "$srcbase/dir/file"
|
||||
+sleep 1
|
||||
+touch "$srcbase/dir/file"
|
||||
+
|
||||
+(cd "$srcbase" && $RSYNC -KRlptzv --rsync-path="$RSYNC" dir/file localhost:) \
|
||||
+ || test_fail "test 2: compressed update through directory symlink failed"
|
||||
+
|
||||
+diff "$srcbase/dir/file" "$HOME/real-dir/file" >/dev/null \
|
||||
+ || test_fail "test 2: compressed update content mismatch"
|
||||
+
|
||||
+######################################################################
|
||||
+# Test 3: Nested directory symlinks (nested/sub/data.txt where
|
||||
+# "nested" is a symlink to "nested_real")
|
||||
+######################################################################
|
||||
+
|
||||
+mkdir -p "$HOME/nested_real/sub"
|
||||
+ln -s nested_real "$HOME/nested"
|
||||
+
|
||||
+mkdir -p "$srcbase/nested/sub"
|
||||
+make_testfile "$srcbase/nested/sub/data.txt"
|
||||
+
|
||||
+(cd "$srcbase" && $RSYNC -KRlptv --rsync-path="$RSYNC" nested/sub/data.txt localhost:) \
|
||||
+ || test_fail "test 3: initial nested transfer failed"
|
||||
+
|
||||
+echo "appended nested" >> "$srcbase/nested/sub/data.txt"
|
||||
+sleep 1
|
||||
+touch "$srcbase/nested/sub/data.txt"
|
||||
+
|
||||
+(cd "$srcbase" && $RSYNC -KRlptv --rsync-path="$RSYNC" nested/sub/data.txt localhost:) \
|
||||
+ || test_fail "test 3: update through nested directory symlink failed"
|
||||
+
|
||||
+diff "$srcbase/nested/sub/data.txt" "$HOME/nested_real/sub/data.txt" >/dev/null \
|
||||
+ || test_fail "test 3: nested update content mismatch"
|
||||
+
|
||||
+######################################################################
|
||||
+# Test 4: --backup with directory symlinks
|
||||
+#
|
||||
+# Exercises the finish_transfer() "fnamecmp == fname" pointer
|
||||
+# comparison that determines whether to update fnamecmp to the
|
||||
+# backup name. If broken, --backup would reference a renamed file
|
||||
+# for xattr handling.
|
||||
+######################################################################
|
||||
+
|
||||
+# Reset destination
|
||||
+rm -f "$HOME/real-dir/file" "$HOME/real-dir/file~"
|
||||
+
|
||||
+make_testfile "$srcbase/dir/file"
|
||||
+
|
||||
+(cd "$srcbase" && $RSYNC -KRlptv --rsync-path="$RSYNC" dir/file localhost:) \
|
||||
+ || test_fail "test 4: initial transfer for backup test failed"
|
||||
+
|
||||
+echo "backup update" >> "$srcbase/dir/file"
|
||||
+sleep 1
|
||||
+touch "$srcbase/dir/file"
|
||||
+
|
||||
+(cd "$srcbase" && $RSYNC -KRlptv --backup --rsync-path="$RSYNC" dir/file localhost:) \
|
||||
+ || test_fail "test 4: update with --backup through directory symlink failed"
|
||||
+
|
||||
+diff "$srcbase/dir/file" "$HOME/real-dir/file" >/dev/null \
|
||||
+ || test_fail "test 4: backup update content mismatch"
|
||||
+
|
||||
+if [ ! -f "$HOME/real-dir/file~" ]; then
|
||||
+ test_fail "test 4: backup file was not created"
|
||||
+fi
|
||||
+
|
||||
+######################################################################
|
||||
+# Test 5: --inplace with directory symlinks
|
||||
+#
|
||||
+# Exercises the updating_basis_or_equiv check which uses
|
||||
+# "fnamecmp == fname". With --inplace, rsync writes directly to
|
||||
+# the destination file instead of a temp file.
|
||||
+######################################################################
|
||||
+
|
||||
+rm -f "$HOME/real-dir/file" "$HOME/real-dir/file~"
|
||||
+
|
||||
+make_testfile "$srcbase/dir/file"
|
||||
+
|
||||
+(cd "$srcbase" && $RSYNC -KRlptv --inplace --rsync-path="$RSYNC" dir/file localhost:) \
|
||||
+ || test_fail "test 5: initial inplace transfer failed"
|
||||
+
|
||||
+echo "inplace update" >> "$srcbase/dir/file"
|
||||
+sleep 1
|
||||
+touch "$srcbase/dir/file"
|
||||
+
|
||||
+(cd "$srcbase" && $RSYNC -KRlptv --inplace --rsync-path="$RSYNC" dir/file localhost:) \
|
||||
+ || test_fail "test 5: inplace update through directory symlink failed"
|
||||
+
|
||||
+diff "$srcbase/dir/file" "$HOME/real-dir/file" >/dev/null \
|
||||
+ || test_fail "test 5: inplace update content mismatch"
|
||||
+
|
||||
+######################################################################
|
||||
+# Test 6: Top-level file (no dirname, no split needed)
|
||||
+#
|
||||
+# Ensures the dirname/basename split is not attempted for files
|
||||
+# at the top level (file->dirname is NULL).
|
||||
+######################################################################
|
||||
+
|
||||
+make_testfile "$srcbase/topfile"
|
||||
+mkdir -p "$HOME"
|
||||
+
|
||||
+(cd "$srcbase" && $RSYNC -Rlptv --rsync-path="$RSYNC" topfile localhost:) \
|
||||
+ || test_fail "test 6: initial top-level transfer failed"
|
||||
+
|
||||
+echo "toplevel update" >> "$srcbase/topfile"
|
||||
+sleep 1
|
||||
+touch "$srcbase/topfile"
|
||||
+
|
||||
+(cd "$srcbase" && $RSYNC -Rlptv --rsync-path="$RSYNC" topfile localhost:) \
|
||||
+ || test_fail "test 6: top-level update failed"
|
||||
+
|
||||
+diff "$srcbase/topfile" "$HOME/topfile" >/dev/null \
|
||||
+ || test_fail "test 6: top-level update content mismatch"
|
||||
+
|
||||
+######################################################################
|
||||
+# Test 7: --partial-dir with protocol < 29
|
||||
+#
|
||||
+# For protocol < 29, fnamecmp_type stays FNAMECMP_FNAME even when
|
||||
+# fnamecmp is set to partialptr. The dirname/basename split must
|
||||
+# NOT trigger in this case (guarded by "fnamecmp == fname").
|
||||
+######################################################################
|
||||
+
|
||||
+rm -f "$HOME/real-dir/file"
|
||||
+make_testfile "$srcbase/dir/file"
|
||||
+
|
||||
+(cd "$srcbase" && $RSYNC -KRlptv --protocol=28 --partial-dir=.rsync-partial \
|
||||
+ --rsync-path="$RSYNC" dir/file localhost:) \
|
||||
+ || test_fail "test 7: initial proto28 partial-dir transfer failed"
|
||||
+
|
||||
+echo "partial-dir update" >> "$srcbase/dir/file"
|
||||
+sleep 1
|
||||
+touch "$srcbase/dir/file"
|
||||
+
|
||||
+(cd "$srcbase" && $RSYNC -KRlptv --protocol=28 --partial-dir=.rsync-partial \
|
||||
+ --rsync-path="$RSYNC" dir/file localhost:) \
|
||||
+ || test_fail "test 7: proto28 partial-dir update through dirlink failed"
|
||||
+
|
||||
+diff "$srcbase/dir/file" "$HOME/real-dir/file" >/dev/null \
|
||||
+ || test_fail "test 7: proto28 partial-dir update content mismatch"
|
||||
+
|
||||
+######################################################################
|
||||
+# Test 8: Protocol < 29 basic directory symlink update
|
||||
+#
|
||||
+# Exercises the protocol < 29 code path and its fallback logic
|
||||
+# (clearing basedir on retry).
|
||||
+######################################################################
|
||||
+
|
||||
+rm -f "$HOME/real-dir/file"
|
||||
+make_testfile "$srcbase/dir/file"
|
||||
+
|
||||
+(cd "$srcbase" && $RSYNC -KRlptv --protocol=28 \
|
||||
+ --rsync-path="$RSYNC" dir/file localhost:) \
|
||||
+ || test_fail "test 8: initial proto28 transfer failed"
|
||||
+
|
||||
+echo "proto28 update" >> "$srcbase/dir/file"
|
||||
+sleep 1
|
||||
+touch "$srcbase/dir/file"
|
||||
+
|
||||
+(cd "$srcbase" && $RSYNC -KRlptv --protocol=28 \
|
||||
+ --rsync-path="$RSYNC" dir/file localhost:) \
|
||||
+ || test_fail "test 8: proto28 update through directory symlink failed"
|
||||
+
|
||||
+diff "$srcbase/dir/file" "$HOME/real-dir/file" >/dev/null \
|
||||
+ || test_fail "test 8: proto28 update content mismatch"
|
||||
+
|
||||
+# The script would have aborted on error, so getting here means we've won.
|
||||
+exit 0
|
||||
@ -1,35 +0,0 @@
|
||||
From 07de42ef075f8f27d45d5e2818f44f120aa08012 Mon Sep 17 00:00:00 2001
|
||||
From: Andrew Tridgell <andrew@tridgell.net>
|
||||
Date: Wed, 22 Apr 2026 09:57:45 +1000
|
||||
Subject: [PATCH] xattrs: fixed count in qsort
|
||||
|
||||
this fixes the count passed to the sort of the xattr list. This issue
|
||||
was reported here:
|
||||
|
||||
https://www.openwall.com/lists/oss-security/2026/04/16/2
|
||||
|
||||
the bug is not exploitable due to the fork-per-connection design of
|
||||
rsync, the attack is the equivalent of the user closing the socket
|
||||
themselves.
|
||||
---
|
||||
xattrs.c | 4 ++--
|
||||
1 file changed, 2 insertions(+), 2 deletions(-)
|
||||
|
||||
diff --git a/xattrs.c b/xattrs.c
|
||||
index 1f2bfacd..aee69622 100644
|
||||
--- a/xattrs.c
|
||||
+++ b/xattrs.c
|
||||
@@ -864,8 +864,8 @@ void receive_xattr(int f, struct file_struct *file)
|
||||
rxa->num = num;
|
||||
}
|
||||
|
||||
- if (need_sort && count > 1)
|
||||
- qsort(temp_xattr.items, count, sizeof (rsync_xa), rsync_xal_compare_names);
|
||||
+ if (need_sort && temp_xattr.count > 1)
|
||||
+ qsort(temp_xattr.items, temp_xattr.count, sizeof (rsync_xa), rsync_xal_compare_names);
|
||||
|
||||
ndx = rsync_xal_store(&temp_xattr); /* adds item to rsync_xal_l */
|
||||
|
||||
--
|
||||
2.53.0
|
||||
|
||||
623
SOURCES/rsync-3.2.5-fix-cve-2026-29518-regressions.patch
Normal file
623
SOURCES/rsync-3.2.5-fix-cve-2026-29518-regressions.patch
Normal file
@ -0,0 +1,623 @@
|
||||
From ba9dd4ec47c6e49f21e5905a91af68aad3c4678e Mon Sep 17 00:00:00 2001
|
||||
From: Andrew Tridgell <andrew@tridgell.net>
|
||||
Date: Sun, 24 May 2026 08:48:42 +1000
|
||||
Subject: [PATCH 61/81] receiver: fix absolute --partial-dir delta resume
|
||||
(false verification)
|
||||
|
||||
A delta (--no-whole-file) resume whose basis is an absolute --partial-dir
|
||||
looped forever on exit code 23 ("failed verification -- update put into
|
||||
partial-dir"), stranding the correct data in the partial-dir and never
|
||||
populating the destination.
|
||||
|
||||
Cause: an absolute --partial-dir makes the basis path absolute, but the
|
||||
receiver opened it with secure_relative_open(NULL, fnamecmp, ...), which by
|
||||
design rejects an absolute relpath (EINVAL). The basis fd was then -1, so
|
||||
receive_data() mapped no basis and (because the matched-block sum_update() is
|
||||
guarded by "if (mapbuf)") computed the whole-file verification checksum over
|
||||
the literal data only -> a spurious mismatch every run. (The data itself was
|
||||
correct, since the in-place update leaves the matched basis bytes in place.)
|
||||
Under a non-chroot daemon the in-place write went through the same call and
|
||||
failed outright.
|
||||
|
||||
Fix: add secure_basis_open(), which treats an operator-trusted absolute basis
|
||||
path as (trusted directory + confined leaf) -- the same way secure_relative_open
|
||||
already trusts an absolute basedir while keeping O_NOFOLLOW on the leaf -- and
|
||||
use it for both the basis read and the inplace-partial write. The strict
|
||||
"reject absolute relpath" contract of secure_relative_open is left intact.
|
||||
|
||||
Defense-in-depth: receive_data() now treats a block-match token with no mapped
|
||||
basis as a protocol inconsistency (it can only arise from a basis that the
|
||||
generator opened but the receiver could not), failing cleanly instead of
|
||||
silently dropping those bytes from the verify checksum or the output.
|
||||
|
||||
Thanks to @sylvain-ilm for the report (#724, #725).
|
||||
|
||||
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||||
(cherry picked from commit eee05c177aeef1abc6e421e47cbf4af9a8135eb5)
|
||||
---
|
||||
receiver.c | 57 +++++++++++++++++++++++++++++++++++++++++++++++++++---
|
||||
1 file changed, 54 insertions(+), 3 deletions(-)
|
||||
|
||||
diff --git a/receiver.c b/receiver.c
|
||||
index 89d2515b..56341a68 100644
|
||||
--- a/receiver.c
|
||||
+++ b/receiver.c
|
||||
@@ -83,6 +83,44 @@ static int updating_basis_or_equiv;
|
||||
#define MAX_UNIQUE_NUMBER 999999
|
||||
#define MAX_UNIQUE_LOOP 100
|
||||
|
||||
+/* Open a basis/output path that may legitimately be an operator-trusted
|
||||
+ * ABSOLUTE path -- e.g. an absolute --partial-dir ("a directory reserved for
|
||||
+ * partial-dir work") or --backup-dir. secure_relative_open() deliberately
|
||||
+ * rejects an absolute relpath, so feeding it the whole absolute partialptr
|
||||
+ * (with a NULL basedir) returns EINVAL: the basis fd is then -1, no basis is
|
||||
+ * mapped, and receive_data() omits every matched block from the whole-file
|
||||
+ * verification checksum -> a spurious "failed verification" that strands the
|
||||
+ * (correct) data in the partial-dir forever.
|
||||
+ *
|
||||
+ * The operator's directory is trusted; only the leaf basename is peer-supplied.
|
||||
+ * So when basedir is NULL and relpath is absolute, split it into its directory
|
||||
+ * (trusted) and leaf and confine just the leaf -- exactly how secure_relative_
|
||||
+ * open already trusts an absolute basedir while O_NOFOLLOW-confining the leaf.
|
||||
+ * Anything else is a straight pass-through that preserves the strict contract. */
|
||||
+static int secure_basis_open(const char *basedir, const char *relpath, int flags, mode_t mode)
|
||||
+{
|
||||
+ if (!basedir && relpath && *relpath == '/') {
|
||||
+ const char *slash = strrchr(relpath, '/');
|
||||
+ const char *leaf = slash + 1;
|
||||
+ char dirbuf[MAXPATHLEN];
|
||||
+ const char *dir;
|
||||
+ if (slash == relpath)
|
||||
+ dir = "/";
|
||||
+ else {
|
||||
+ size_t dlen = slash - relpath;
|
||||
+ if (dlen >= sizeof dirbuf) {
|
||||
+ errno = ENAMETOOLONG;
|
||||
+ return -1;
|
||||
+ }
|
||||
+ memcpy(dirbuf, relpath, dlen);
|
||||
+ dirbuf[dlen] = '\0';
|
||||
+ dir = dirbuf;
|
||||
+ }
|
||||
+ return secure_relative_open(dir, leaf, flags, mode);
|
||||
+ }
|
||||
+ return secure_relative_open(basedir, relpath, flags, mode);
|
||||
+}
|
||||
+
|
||||
/* get_tmpname() - create a tmp filename for a given filename
|
||||
*
|
||||
* If a tmpdir is defined, use that as the directory to put it in. Otherwise,
|
||||
@@ -364,6 +402,18 @@ static int receive_data(int f_in, char *fname_r, int fd_r, OFF_T size_r,
|
||||
|
||||
stats.matched_data += len;
|
||||
|
||||
+ /* A block match can only be honored if we actually mapped the
|
||||
+ * basis. If we didn't (basis open failed), the sender should
|
||||
+ * never have been told a basis existed -- treat it as a protocol
|
||||
+ * inconsistency rather than silently omitting these bytes from
|
||||
+ * the verification checksum (which yields a spurious failure) or
|
||||
+ * leaving a hole in the output. */
|
||||
+ if (!mapbuf) {
|
||||
+ rprintf(FERROR, "got a block match with no basis file for %s [%s]\n",
|
||||
+ full_fname(fname), who_am_i());
|
||||
+ exit_cleanup(RERR_PROTOCOL);
|
||||
+ }
|
||||
+
|
||||
if (DEBUG_GTE(DELTASUM, 3)) {
|
||||
rprintf(FINFO,
|
||||
"chunk[%d] of size %ld at %s offset=%s%s\n",
|
||||
@@ -793,8 +843,9 @@ int recv_files(int f_in, int f_out, char *local_name)
|
||||
fnamecmp = fname;
|
||||
}
|
||||
|
||||
- /* open the file */
|
||||
- fd1 = secure_relative_open(basedir, fnamecmp, O_RDONLY, 0);
|
||||
+ /* open the file (secure_basis_open tolerates an operator-trusted
|
||||
+ * absolute fnamecmp, e.g. an absolute --partial-dir basis) */
|
||||
+ fd1 = secure_basis_open(basedir, fnamecmp, O_RDONLY, 0);
|
||||
|
||||
if (fd1 == -1 && protocol_version < 29) {
|
||||
if (fnamecmp != fname) {
|
||||
@@ -884,7 +935,7 @@ int recv_files(int f_in, int f_out, char *local_name)
|
||||
* attacker could switch a directory to a symlink between
|
||||
* path validation and file open. */
|
||||
if (use_secure_symlinks)
|
||||
- fd2 = secure_relative_open(NULL, fnametmp, O_WRONLY|O_CREAT, 0600);
|
||||
+ fd2 = secure_basis_open(NULL, fnametmp, O_WRONLY|O_CREAT, 0600);
|
||||
else
|
||||
fd2 = do_open(fnametmp, O_WRONLY|O_CREAT, 0600);
|
||||
#ifdef linux
|
||||
--
|
||||
2.53.0
|
||||
|
||||
From ab7d8e9df3dd1f2d2acc56c49c7d0c70420f8883 Mon Sep 17 00:00:00 2001
|
||||
From: Andrew Tridgell <andrew@tridgell.net>
|
||||
Date: Wed, 3 Jun 2026 20:48:10 +1000
|
||||
Subject: [PATCH 63/81] sender: open a module-root-absolute path for a `path =
|
||||
/` module (#897)
|
||||
|
||||
A daemon module with path=/ makes F_PATHNAME absolute, so the secure_path built
|
||||
for the content open starts with '/'. secure_relative_open() rejects an
|
||||
absolute relpath with EINVAL, so a use-chroot=no daemon with path=/ could not
|
||||
send any file ('failed to open ...: Invalid argument (22)') -- a regression
|
||||
from 3.4.2. Strip leading slashes to a module-relative path; resolution stays
|
||||
confined beneath module_dir.
|
||||
|
||||
Thanks to @moonlitbugs for the report (#897).
|
||||
|
||||
(cherry picked from commit 9886a06610370b3219ea9a29753388e261f4853d)
|
||||
---
|
||||
sender.c | 9 ++++++++-
|
||||
1 file changed, 8 insertions(+), 1 deletion(-)
|
||||
|
||||
diff --git a/sender.c b/sender.c
|
||||
index 033f87e5..32913af0 100644
|
||||
--- a/sender.c
|
||||
+++ b/sender.c
|
||||
@@ -362,6 +362,7 @@ void send_files(int f_in, int f_out)
|
||||
* Reconstruct the full path relative to module_dir
|
||||
* from F_PATHNAME (path) and f_name (fname). */
|
||||
char secure_path[MAXPATHLEN];
|
||||
+ const char *relp;
|
||||
int slen = snprintf(secure_path, sizeof secure_path, "%s%s%s", path, slash, fname);
|
||||
if (slen >= (int)sizeof secure_path) {
|
||||
io_error |= IOERR_GENERAL;
|
||||
@@ -371,7 +372,13 @@ void send_files(int f_in, int f_out)
|
||||
send_msg_int(MSG_NO_SEND, ndx);
|
||||
continue;
|
||||
}
|
||||
- fd = secure_relative_open(module_dir, secure_path, O_RDONLY, 0);
|
||||
+ /* A module with `path = /` makes F_PATHNAME absolute, so the
|
||||
+ * joined path starts with '/'; strip leading slashes to a
|
||||
+ * module-relative path that secure_relative_open accepts (#897). */
|
||||
+ relp = secure_path;
|
||||
+ while (*relp == '/')
|
||||
+ relp++;
|
||||
+ fd = secure_relative_open(module_dir, relp, O_RDONLY, 0);
|
||||
} else {
|
||||
fd = do_open_checklinks(fname);
|
||||
}
|
||||
--
|
||||
2.53.0
|
||||
|
||||
From 57128940b1c63c726cdedd5a4531d2ff1ed7c5e3 Mon Sep 17 00:00:00 2001
|
||||
From: Andrew Tridgell <andrew@tridgell.net>
|
||||
Date: Wed, 3 Jun 2026 20:48:10 +1000
|
||||
Subject: [PATCH 64/81] syscall/receiver: honour a relative alt-basis dir on a
|
||||
daemon receiver (#915)
|
||||
|
||||
The symlink-race hardening routed the receiver's basis open through
|
||||
secure_relative_open(), which rejects any '..' -- so a sibling
|
||||
--link-dest=../01 on a use-chroot=no daemon was silently ignored and every file
|
||||
re-transferred (#915/#928, a regression from 3.4.1).
|
||||
|
||||
Narrow the confinement to the sanitizing daemon (am_daemon && !am_chrooted) and
|
||||
re-anchor it at the module root, the real trust boundary: secure_relative_open()
|
||||
prefixes the cwd's module-relative path (from rsync's logical curr_dir[], a
|
||||
guaranteed lexical prefix of module_dir) and resolves beneath module_dir, so
|
||||
RESOLVE_BENEATH permits an in-module '..' climb while still rejecting one that
|
||||
escapes the module. secure_basis_open() opens with a bare do_open() in the
|
||||
non-sanitizing cases. t_stub.c gains weak curr_dir[]/curr_dir_len for the
|
||||
helpers (via #pragma weak on non-GNU compilers, where rsync.h erases
|
||||
__attribute__).
|
||||
|
||||
Two tests: link-dest-relative-basis asserts the in-module '..' is honoured;
|
||||
link-dest-module-escape asserts a --link-dest=../../OUTSIDE climb that leaves
|
||||
the module is refused (not hard-linked to an outside file). See upstream
|
||||
PR #930.
|
||||
|
||||
Thanks to @fufu65 (#915) and @JetAppsClark (#928) for the reports.
|
||||
|
||||
(cherry picked from commit 948edffb43b56216bcc9932955481ef80a6a69f1)
|
||||
---
|
||||
receiver.c | 23 +++++++++++++-
|
||||
syscall.c | 91 +++++++++++++++++++++++++++++++++++++++++++++++++-----
|
||||
t_stub.c | 2 ++
|
||||
util1.c | 4 +--
|
||||
4 files changed, 109 insertions(+), 11 deletions(-)
|
||||
|
||||
diff --git a/receiver.c b/receiver.c
|
||||
index 56341a68..e199914f 100644
|
||||
--- a/receiver.c
|
||||
+++ b/receiver.c
|
||||
@@ -99,6 +99,27 @@ static int updating_basis_or_equiv;
|
||||
* Anything else is a straight pass-through that preserves the strict contract. */
|
||||
static int secure_basis_open(const char *basedir, const char *relpath, int flags, mode_t mode)
|
||||
{
|
||||
+ extern int am_daemon, am_chrooted;
|
||||
+
|
||||
+ /* The confined resolver is only needed for the sanitizing daemon
|
||||
+ * (am_daemon && !am_chrooted, i.e. use_secure_symlinks). Local /
|
||||
+ * remote-shell mode has no module boundary, and "use chroot = yes" makes
|
||||
+ * the kernel root the boundary, so there an alt-dest basis like
|
||||
+ * --link-dest=../01 must resolve against the cwd as a bare open did before
|
||||
+ * the hardening (confining it would reject the legitimate sibling "..",
|
||||
+ * #915). */
|
||||
+ if (!am_daemon || am_chrooted) {
|
||||
+ if (basedir) {
|
||||
+ char fullpath[MAXPATHLEN];
|
||||
+ if (pathjoin(fullpath, sizeof fullpath, basedir, relpath) >= sizeof fullpath) {
|
||||
+ errno = ENAMETOOLONG;
|
||||
+ return -1;
|
||||
+ }
|
||||
+ return do_open(fullpath, flags, mode);
|
||||
+ }
|
||||
+ return do_open(relpath, flags, mode);
|
||||
+ }
|
||||
+
|
||||
if (!basedir && relpath && *relpath == '/') {
|
||||
const char *slash = strrchr(relpath, '/');
|
||||
const char *leaf = slash + 1;
|
||||
@@ -859,7 +880,7 @@ int recv_files(int f_in, int f_out, char *local_name)
|
||||
basedir = basis_dir[0];
|
||||
fnamecmp = fname;
|
||||
fnamecmp_type = FNAMECMP_BASIS_DIR_LOW;
|
||||
- fd1 = secure_relative_open(basedir, fnamecmp, O_RDONLY, 0);
|
||||
+ fd1 = secure_basis_open(basedir, fnamecmp, O_RDONLY, 0);
|
||||
}
|
||||
}
|
||||
|
||||
diff --git a/syscall.c b/syscall.c
|
||||
index 47777350..4f456807 100644
|
||||
--- a/syscall.c
|
||||
+++ b/syscall.c
|
||||
@@ -1761,13 +1761,68 @@ static int secure_relative_open_resolve_beneath(const char *basedir, const char
|
||||
}
|
||||
#endif
|
||||
|
||||
+/* The logical current directory (maintained by change_dir() in util1.c).
|
||||
+ * Defined here -- rather than in util1.c -- so the test helpers that link
|
||||
+ * syscall.o but not util1.o (tls, trimslash) get the definition without a
|
||||
+ * weak-symbol fallback, which is not portable to PE/COFF targets (Cygwin). */
|
||||
+char curr_dir[MAXPATHLEN];
|
||||
+unsigned int curr_dir_len;
|
||||
+
|
||||
int secure_relative_open(const char *basedir, const char *relpath, int flags, mode_t mode)
|
||||
{
|
||||
+ extern int am_daemon, am_chrooted;
|
||||
+ extern char *module_dir;
|
||||
+ extern unsigned int module_dirlen;
|
||||
+ char modrel_buf[MAXPATHLEN];
|
||||
+ int reanchored = 0;
|
||||
+
|
||||
if (!relpath || relpath[0] == '/') {
|
||||
// must be a relative path
|
||||
errno = EINVAL;
|
||||
return -1;
|
||||
}
|
||||
+
|
||||
+ /* Sanitizing daemon only (am_daemon && !am_chrooted). Here we have chdir'd
|
||||
+ * into a sub-dir of the module (the transfer destination), so a relative
|
||||
+ * alt-dest like "../01" may legitimately climb to a sibling that is still
|
||||
+ * inside the module (#915). Confining beneath the cwd would reject that
|
||||
+ * climb. Re-anchor at the module root -- the real trust boundary -- by
|
||||
+ * prefixing the cwd's module-relative path (from rsync's logical curr_dir[],
|
||||
+ * a guaranteed lexical prefix of module_dir, unlike getcwd()) and resolving
|
||||
+ * beneath module_dir; RESOLVE_BENEATH then allows in-module climbs and still
|
||||
+ * rejects escapes. Only for paths that contain "..". module_dirlen is 0 for
|
||||
+ * a `path = /` module (clientserver.c), so we gate on module_dir, not its
|
||||
+ * length, to cover that case too -- the prefix check below treats
|
||||
+ * module_dirlen 0 as "module root is /". */
|
||||
+ if (am_daemon && !am_chrooted
|
||||
+ && module_dir && module_dir[0] == '/'
|
||||
+ && (basedir == NULL || basedir[0] != '/')
|
||||
+ && (path_has_dotdot_component(relpath)
|
||||
+ || (basedir && path_has_dotdot_component(basedir)))) {
|
||||
+ const char *p;
|
||||
+ int n;
|
||||
+ if (curr_dir_len >= module_dirlen
|
||||
+ && strncmp(curr_dir, module_dir, module_dirlen) == 0
|
||||
+ && (curr_dir[module_dirlen] == '\0' || curr_dir[module_dirlen] == '/')) {
|
||||
+ for (p = curr_dir + module_dirlen; *p == '/'; p++) {}
|
||||
+ if (basedir)
|
||||
+ n = snprintf(modrel_buf, sizeof modrel_buf, "%s%s%s/%s",
|
||||
+ p, *p ? "/" : "", basedir, relpath);
|
||||
+ else
|
||||
+ n = snprintf(modrel_buf, sizeof modrel_buf, "%s%s%s",
|
||||
+ p, *p ? "/" : "", relpath);
|
||||
+ if (n < 0 || n >= (int)sizeof modrel_buf) {
|
||||
+ errno = ENAMETOOLONG;
|
||||
+ return -1;
|
||||
+ }
|
||||
+ basedir = module_dir; /* absolute, operator-trusted anchor */
|
||||
+ relpath = modrel_buf;
|
||||
+ reanchored = 1;
|
||||
+ }
|
||||
+ /* else: cwd not under module root as expected -- fall through to the
|
||||
+ * front-door rejection below (fail safe). */
|
||||
+ }
|
||||
+
|
||||
/* Reject any path with a literal ".." component (bare "..",
|
||||
* "../foo", "foo/..", "foo/../bar", "subdir/.."). The previous
|
||||
* substring-based check caught only "../" prefix and "/../"
|
||||
@@ -1776,14 +1831,19 @@ int secure_relative_open(const char *basedir, const char *relpath, int flags, mo
|
||||
* and pre-5.6 Linux. RESOLVE_BENEATH on Linux/FreeBSD/macOS
|
||||
* catches some of these in-kernel with EXDEV, but the front
|
||||
* door must reject them consistently with EINVAL across all
|
||||
- * platforms so callers can rely on the validation. */
|
||||
- if (path_has_dotdot_component(relpath)) {
|
||||
- errno = EINVAL;
|
||||
- return -1;
|
||||
- }
|
||||
- if (basedir && basedir[0] != '/' && path_has_dotdot_component(basedir)) {
|
||||
- errno = EINVAL;
|
||||
- return -1;
|
||||
+ * platforms so callers can rely on the validation. Skipped for a
|
||||
+ * re-anchored path: its ".." is deliberate, stays within the module,
|
||||
+ * and is adjudicated by RESOLVE_BENEATH below (the portable fallback
|
||||
+ * re-rejects it -- see there). */
|
||||
+ if (!reanchored) {
|
||||
+ if (path_has_dotdot_component(relpath)) {
|
||||
+ errno = EINVAL;
|
||||
+ return -1;
|
||||
+ }
|
||||
+ if (basedir && basedir[0] != '/' && path_has_dotdot_component(basedir)) {
|
||||
+ errno = EINVAL;
|
||||
+ return -1;
|
||||
+ }
|
||||
}
|
||||
|
||||
#ifdef __linux__
|
||||
@@ -1800,6 +1860,21 @@ int secure_relative_open(const char *basedir, const char *relpath, int flags, mo
|
||||
return secure_relative_open_resolve_beneath(basedir, relpath, flags, mode);
|
||||
#endif
|
||||
|
||||
+ /* Portable fallback only (no kernel RESOLVE_BENEATH): the per-component
|
||||
+ * O_NOFOLLOW walk below can't adjudicate ".." safely, so reject it here --
|
||||
+ * even for a re-anchored path. This re-breaks --link-dest=../01 on
|
||||
+ * openat2/O_RESOLVE_BENEATH-less platforms (NetBSD/OpenBSD/Solaris/Cygwin/
|
||||
+ * pre-5.6 Linux), trading function for safety; on the kernel paths above
|
||||
+ * RESOLVE_BENEATH already allowed the in-module climb. */
|
||||
+ if (path_has_dotdot_component(relpath)) {
|
||||
+ errno = EINVAL;
|
||||
+ return -1;
|
||||
+ }
|
||||
+ if (basedir && basedir[0] != '/' && path_has_dotdot_component(basedir)) {
|
||||
+ errno = EINVAL;
|
||||
+ return -1;
|
||||
+ }
|
||||
+
|
||||
#if !defined(O_NOFOLLOW) || !defined(O_DIRECTORY) || !defined(AT_FDCWD)
|
||||
// really old system, all we can do is live with the risks
|
||||
if (!basedir) {
|
||||
diff --git a/t_stub.c b/t_stub.c
|
||||
index 904dac99..32659b4a 100644
|
||||
--- a/t_stub.c
|
||||
+++ b/t_stub.c
|
||||
@@ -39,6 +39,8 @@ int open_noatime = 0;
|
||||
size_t max_alloc = 0; /* max_alloc is needed when combined with util2.o */
|
||||
char *partial_dir;
|
||||
char *module_dir;
|
||||
+/* curr_dir[]/curr_dir_len (read by secure_relative_open) are defined in
|
||||
+ * syscall.c, which every helper links -- no stub needed here. */
|
||||
filter_rule_list daemon_filter_list;
|
||||
|
||||
void rprintf(UNUSED(enum logcode code), const char *format, ...)
|
||||
diff --git a/util1.c b/util1.c
|
||||
index 36c1b68c..12361057 100644
|
||||
--- a/util1.c
|
||||
+++ b/util1.c
|
||||
@@ -41,8 +41,8 @@ extern filter_rule_list daemon_filter_list;
|
||||
|
||||
int sanitize_paths = 0;
|
||||
|
||||
-char curr_dir[MAXPATHLEN];
|
||||
-unsigned int curr_dir_len;
|
||||
+extern char curr_dir[MAXPATHLEN]; /* defined in syscall.c */
|
||||
+extern unsigned int curr_dir_len;
|
||||
int curr_dir_depth; /* This is only set for a sanitizing daemon. */
|
||||
|
||||
/* Set a fd into nonblocking mode. */
|
||||
--
|
||||
2.53.0
|
||||
|
||||
From 005d118f2d25ceaf7680f5b7bb92d70bacf97660 Mon Sep 17 00:00:00 2001
|
||||
From: pterror <pterrorbird@gmail.com>
|
||||
Date: Fri, 5 Jun 2026 17:24:05 +1000
|
||||
Subject: [PATCH 75/81] receiver: fix NULL deref on the delta discard path
|
||||
|
||||
receive_data() crashed a receiver that was merely DISCARDING a file's
|
||||
delta stream. discard_receive_data() calls receive_data() with
|
||||
fname == NULL and fd == -1, so size_r == 0 and mapbuf == NULL. A normal
|
||||
block-MATCH token (against a block the basis and source share) then
|
||||
reaches the !mapbuf branch added in 31fbb17d ("receiver: fix absolute
|
||||
--partial-dir delta resume"), which calls full_fname(fname). full_fname()
|
||||
dereferences its argument unconditionally (util1.c: `if (*fn == '/')`),
|
||||
so fname == NULL faults there -> receiver SIGSEGV.
|
||||
|
||||
This is a normal-operation crash with a stock cooperating sender, not an
|
||||
adversarial one. The generator hands the sender real block sums whenever
|
||||
the basis is readable and we're in delta mode; the receiver only decides
|
||||
to discard afterwards, when its output cannot be produced -- e.g. the
|
||||
destination directory is not writable (mkstemp fails), the basis turns
|
||||
out to be a directory, or a --partial-dir resume is skipped. A MATCH
|
||||
token arriving during that discard hit the NULL deref.
|
||||
|
||||
The 31fbb17d branch is correct only for a REAL output transfer (fd != -1,
|
||||
fname valid): there, a block match with no mapped basis is a genuine
|
||||
protocol inconsistency (the generator promised a basis the receiver could
|
||||
not open), and honoring it would silently omit those bytes from the
|
||||
verification checksum or leave a hole, so hard-erroring -- and
|
||||
full_fname(fname) -- is right. It conflated that with the discard path.
|
||||
|
||||
The discriminator is fd, not mapbuf: on the discard path fd == -1 always;
|
||||
on the real-output inconsistency fd != -1. Scope the "no basis file"
|
||||
protocol error to fd != -1 (where fname is non-NULL and full_fname is
|
||||
safe) and, on the discard path (fd == -1), absorb the matched bytes
|
||||
benignly (offset += len; continue) -- symmetric with the literal-token
|
||||
handling just above, and restoring the pre-31fbb17d behavior. The
|
||||
real-transfer inconsistency check is preserved unchanged.
|
||||
|
||||
(cherry picked from commit 26f13bc148a0aa5d00496543cdfe6024fa269a11)
|
||||
---
|
||||
receiver.c | 34 +++++++++++++++++++++++++---------
|
||||
1 file changed, 25 insertions(+), 9 deletions(-)
|
||||
|
||||
diff --git a/receiver.c b/receiver.c
|
||||
index e199914f..e1f1fb38 100644
|
||||
--- a/receiver.c
|
||||
+++ b/receiver.c
|
||||
@@ -423,16 +423,32 @@ static int receive_data(int f_in, char *fname_r, int fd_r, OFF_T size_r,
|
||||
|
||||
stats.matched_data += len;
|
||||
|
||||
- /* A block match can only be honored if we actually mapped the
|
||||
- * basis. If we didn't (basis open failed), the sender should
|
||||
- * never have been told a basis existed -- treat it as a protocol
|
||||
- * inconsistency rather than silently omitting these bytes from
|
||||
- * the verification checksum (which yields a spurious failure) or
|
||||
- * leaving a hole in the output. */
|
||||
+ /* A block match with no mapped basis is a protocol inconsistency
|
||||
+ * ONLY when we are actually producing output (fd != -1): the
|
||||
+ * generator told the sender a basis existed but the receiver could
|
||||
+ * not open it, so honoring the match would silently omit these
|
||||
+ * bytes from the verification checksum (a spurious failure) or
|
||||
+ * leave a hole in the output. Fail cleanly in that case.
|
||||
+ *
|
||||
+ * On the DISCARD path (fd == -1, fname == NULL) there is no output
|
||||
+ * and no verification: discard_receive_data() deliberately drains a
|
||||
+ * delta the receiver never intends to write (basis fstat failed,
|
||||
+ * basis is a directory, output open failed, batch skip, ...). The
|
||||
+ * sender does not know the data is being discarded and streams an
|
||||
+ * ordinary delta, so a match token here is NORMAL protocol, not
|
||||
+ * malformed. Absorb it benignly (advance the offset and continue),
|
||||
+ * as the pre-existing "if (mapbuf)" guards did before this check was
|
||||
+ * added in 31fbb17d -- erroring would wrongly break legitimate
|
||||
+ * transfers, and full_fname(fname) with fname==NULL would
|
||||
+ * dereference NULL (a receiver crash on a normal transfer). */
|
||||
if (!mapbuf) {
|
||||
- rprintf(FERROR, "got a block match with no basis file for %s [%s]\n",
|
||||
- full_fname(fname), who_am_i());
|
||||
- exit_cleanup(RERR_PROTOCOL);
|
||||
+ if (fd != -1) {
|
||||
+ rprintf(FERROR, "got a block match with no basis file for %s [%s]\n",
|
||||
+ full_fname(fname), who_am_i());
|
||||
+ exit_cleanup(RERR_PROTOCOL);
|
||||
+ }
|
||||
+ offset += len;
|
||||
+ continue;
|
||||
}
|
||||
|
||||
if (DEBUG_GTE(DELTASUM, 3)) {
|
||||
--
|
||||
2.53.0
|
||||
|
||||
From 0a5fa00fdcbacbebb89daca0ae68ae320f22dc74 Mon Sep 17 00:00:00 2001
|
||||
From: Andrew Tridgell <andrew@tridgell.net>
|
||||
Date: Tue, 5 May 2026 16:48:16 +1000
|
||||
Subject: [PATCH 46/81] receiver: add parent_ndx<0 guard, mirroring 797e17f
|
||||
|
||||
Commit 797e17f ("fixed an invalid access to files array") added a
|
||||
parent_ndx < 0 guard to send_files() in sender.c, but the visually-
|
||||
identical block in recv_files() in receiver.c was not updated. A
|
||||
malicious rsync:// server can therefore drive any connecting client
|
||||
into the same out-of-bounds dir_flist->files[-1] read followed by a
|
||||
file_struct dereference in f_name() one line later.
|
||||
|
||||
Reach: protocol-30+ default (inc_recurse) makes flist.c:2745 set
|
||||
parent_ndx = -1 on the first received flist when the sender omits a
|
||||
leading "." entry; rsync.c flist_for_ndx() does not reject ndx == 0
|
||||
in that state because the range check evaluates 0 < 0 = false; and
|
||||
read_ndx_and_attrs() only validates ndx with the ITEM_TRANSFER bit
|
||||
set, so iflags=ITEM_IS_NEW (or any other non-transfer iflag word)
|
||||
bypasses the check.
|
||||
|
||||
Apply the same guard receiver-side. Confirmed: the same PoC (a
|
||||
minimal Python rsyncd that handshakes with CF_INC_RECURSE, sends a
|
||||
no-leading-"." flist, and emits ndx=0 with ITEM_IS_NEW) crashes
|
||||
unpatched 3.4.2 with SEGV_MAPERR si_addr=0x4101a-class in the
|
||||
receiver child; with this guard it exits cleanly with code 2
|
||||
(RERR_PROTOCOL).
|
||||
|
||||
The attack surface delta over the sender variant is large:
|
||||
the original was malicious-client -> daemon, this is
|
||||
malicious-server -> any rsync client doing a normal rsync://
|
||||
or remote-shell pull.
|
||||
|
||||
Reported by Pratham Gupta (alchemy1729).
|
||||
|
||||
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||||
---
|
||||
generator.c | 4 ++++
|
||||
io.c | 3 +++
|
||||
receiver.c | 7 ++++++-
|
||||
sender.c | 2 ++
|
||||
4 files changed, 15 insertions(+), 1 deletion(-)
|
||||
|
||||
diff --git a/generator.c b/generator.c
|
||||
index b80eb2e3..38f5ad33 100644
|
||||
--- a/generator.c
|
||||
+++ b/generator.c
|
||||
@@ -2146,6 +2146,8 @@ void check_for_finished_files(int itemizing, enum logcode code, int check_redo)
|
||||
if (send_failed)
|
||||
ndx = get_hlink_num();
|
||||
flist = flist_for_ndx(ndx, "check_for_finished_files.1");
|
||||
+ if (ndx < flist->ndx_start)
|
||||
+ exit_cleanup(RERR_PROTOCOL);
|
||||
file = flist->files[ndx - flist->ndx_start];
|
||||
assert(file->flags & FLAG_HLINKED);
|
||||
if (send_failed)
|
||||
@@ -2174,6 +2176,8 @@ void check_for_finished_files(int itemizing, enum logcode code, int check_redo)
|
||||
|
||||
flist = cur_flist;
|
||||
cur_flist = flist_for_ndx(ndx, "check_for_finished_files.2");
|
||||
+ if (ndx < cur_flist->ndx_start)
|
||||
+ exit_cleanup(RERR_PROTOCOL);
|
||||
|
||||
file = cur_flist->files[ndx - cur_flist->ndx_start];
|
||||
if (solo_file)
|
||||
diff --git a/io.c b/io.c
|
||||
index 8d1cf7f2..2d94c1f4 100644
|
||||
--- a/io.c
|
||||
+++ b/io.c
|
||||
@@ -1090,6 +1090,9 @@ static void got_flist_entry_status(enum festatus status, int ndx)
|
||||
{
|
||||
struct file_list *flist = flist_for_ndx(ndx, "got_flist_entry_status");
|
||||
|
||||
+ if (ndx < flist->ndx_start)
|
||||
+ exit_cleanup(RERR_PROTOCOL);
|
||||
+
|
||||
if (remove_source_files) {
|
||||
active_filecnt--;
|
||||
active_bytecnt -= F_LENGTH(flist->files[ndx - flist->ndx_start]);
|
||||
diff --git a/receiver.c b/receiver.c
|
||||
index 63e5cedb..0a993e0f 100644
|
||||
--- a/receiver.c
|
||||
+++ b/receiver.c
|
||||
@@ -467,7 +467,10 @@ static void handle_delayed_updates(char *local_name)
|
||||
static void no_batched_update(int ndx, BOOL is_redo)
|
||||
{
|
||||
struct file_list *flist = flist_for_ndx(ndx, "no_batched_update");
|
||||
- struct file_struct *file = flist->files[ndx - flist->ndx_start];
|
||||
+ struct file_struct *file;
|
||||
+ if (ndx < flist->ndx_start)
|
||||
+ exit_cleanup(RERR_PROTOCOL);
|
||||
+ file = flist->files[ndx - flist->ndx_start];
|
||||
|
||||
rprintf(FERROR_XFER, "(No batched update for%s \"%s\")\n",
|
||||
is_redo ? " resend of" : "", f_name(file, NULL));
|
||||
@@ -604,6 +607,8 @@ int recv_files(int f_in, int f_out, char *local_name)
|
||||
|
||||
if (ndx - cur_flist->ndx_start >= 0)
|
||||
file = cur_flist->files[ndx - cur_flist->ndx_start];
|
||||
+ else if (cur_flist->parent_ndx < 0)
|
||||
+ exit_cleanup(RERR_PROTOCOL);
|
||||
else
|
||||
file = dir_flist->files[cur_flist->parent_ndx];
|
||||
fname = local_name ? local_name : f_name(file, fbuf);
|
||||
diff --git a/sender.c b/sender.c
|
||||
index 99f431fe..033f87e5 100644
|
||||
--- a/sender.c
|
||||
+++ b/sender.c
|
||||
@@ -140,6 +140,8 @@ void successful_send(int ndx)
|
||||
return;
|
||||
|
||||
flist = flist_for_ndx(ndx, "successful_send");
|
||||
+ if (ndx < flist->ndx_start)
|
||||
+ exit_cleanup(RERR_PROTOCOL);
|
||||
file = flist->files[ndx - flist->ndx_start];
|
||||
if (!change_pathname(file, NULL, 0))
|
||||
return;
|
||||
--
|
||||
2.53.0
|
||||
|
||||
4023
SOURCES/rsync-3.2.5-fix-cve-2026-29518.patch
Normal file
4023
SOURCES/rsync-3.2.5-fix-cve-2026-29518.patch
Normal file
File diff suppressed because it is too large
Load Diff
236
SOURCES/rsync-3.2.5-fix-cve-2026-43618.patch
Normal file
236
SOURCES/rsync-3.2.5-fix-cve-2026-43618.patch
Normal file
@ -0,0 +1,236 @@
|
||||
From 2433971801c8814f2514a97c57dd27b12db1a243 Mon Sep 17 00:00:00 2001
|
||||
From: Andrew Tridgell <andrew@tridgell.net>
|
||||
Date: Wed, 29 Apr 2026 11:10:59 +1000
|
||||
Subject: [PATCH] token: harden compressed-token decoding against integer
|
||||
overflow
|
||||
|
||||
The receiver's three compressed-token decoders --
|
||||
recv_deflated_token (zlib), recv_zstd_token, and
|
||||
recv_compressed_token (lz4) -- accumulated rx_token (a 32-bit
|
||||
signed counter) without overflow checking. A malicious sender
|
||||
could craft a compressed-token stream that walked rx_token past
|
||||
INT32_MAX, with careful manipulation leaking process memory
|
||||
contents to the wire (environment variables, passwords, heap
|
||||
pointers, library pointers -- significantly weakening ASLR
|
||||
and facilitating further exploitation).
|
||||
|
||||
Cap rx_token at MAX_TOKEN_INDEX = 0x7ffffffe. Fold the
|
||||
bookkeeping into recv_compressed_token_num() and
|
||||
recv_compressed_token_run() shared by all three decoders. Reject
|
||||
negative or out-of-range token values explicitly. Also cap the
|
||||
simple_recv_token literal-block length at the source: any
|
||||
wire-supplied length > CHUNK_SIZE is ill-formed (the matching
|
||||
simple_send_token never writes a chunk larger than CHUNK_SIZE),
|
||||
so reject before looping on attacker-controlled bytes.
|
||||
|
||||
Reach: an authenticated daemon connection with compression
|
||||
enabled (the default for protocols >= 30 when both peers
|
||||
advertise it). Disabling compression on the daemon
|
||||
("refuse options = compress" in rsyncd.conf) is the available
|
||||
workaround.
|
||||
|
||||
Reporter: Omar Elsayed (seks99x).
|
||||
|
||||
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||||
---
|
||||
receiver.c | 11 +++++-
|
||||
token.c | 102 ++++++++++++++++++++++++++++++-----------------------
|
||||
2 files changed, 67 insertions(+), 46 deletions(-)
|
||||
|
||||
diff --git a/receiver.c b/receiver.c
|
||||
index 6c74468d..f42e764c 100644
|
||||
--- a/receiver.c
|
||||
+++ b/receiver.c
|
||||
@@ -310,7 +310,12 @@ static int receive_data(int f_in, char *fname_r, int fd_r, OFF_T size_r,
|
||||
}
|
||||
}
|
||||
|
||||
- while ((i = recv_token(f_in, &data)) != 0) {
|
||||
+ while (1) {
|
||||
+ data = NULL;
|
||||
+ i = recv_token(f_in, &data);
|
||||
+ if (i == 0)
|
||||
+ break;
|
||||
+
|
||||
if (INFO_GTE(PROGRESS, 1))
|
||||
show_progress(offset, total_size);
|
||||
|
||||
@@ -318,6 +323,10 @@ static int receive_data(int f_in, char *fname_r, int fd_r, OFF_T size_r,
|
||||
maybe_send_keepalive(time(NULL), MSK_ALLOW_FLUSH | MSK_ACTIVE_RECEIVER);
|
||||
|
||||
if (i > 0) {
|
||||
+ if (!data) {
|
||||
+ rprintf(FERROR, "Invalid literal token with no data [%s]\n", who_am_i());
|
||||
+ exit_cleanup(RERR_PROTOCOL);
|
||||
+ }
|
||||
if (DEBUG_GTE(DELTASUM, 3)) {
|
||||
rprintf(FINFO,"data recv %d at %s\n",
|
||||
i, big_num(offset));
|
||||
diff --git a/token.c b/token.c
|
||||
index f5a41c98..7e295326 100644
|
||||
--- a/token.c
|
||||
+++ b/token.c
|
||||
@@ -286,6 +286,14 @@ static int32 simple_recv_token(int f, char **data)
|
||||
int32 i = read_int(f);
|
||||
if (i <= 0)
|
||||
return i;
|
||||
+ /* simple_send_token caps each literal chunk at CHUNK_SIZE;
|
||||
+ * reject anything larger so a hostile peer cannot drive the
|
||||
+ * read_buf below past our static CHUNK_SIZE buffer. */
|
||||
+ if (i > CHUNK_SIZE) {
|
||||
+ rprintf(FERROR, "invalid uncompressed token length %ld [%s]\n",
|
||||
+ (long)i, who_am_i());
|
||||
+ exit_cleanup(RERR_PROTOCOL);
|
||||
+ }
|
||||
residue = i;
|
||||
}
|
||||
|
||||
@@ -488,9 +496,52 @@ static char *cbuf;
|
||||
static char *dbuf;
|
||||
|
||||
/* for decoding runs of tokens */
|
||||
+#define MAX_TOKEN_INDEX ((int32)0x7ffffffe)
|
||||
+
|
||||
static int32 rx_token;
|
||||
static int32 rx_run;
|
||||
|
||||
+static NORETURN void invalid_compressed_token(void)
|
||||
+{
|
||||
+ rprintf(FERROR, "invalid token number in compressed stream\n");
|
||||
+ exit_cleanup(RERR_PROTOCOL);
|
||||
+}
|
||||
+
|
||||
+static int32 recv_compressed_token_num(int f, int32 flag)
|
||||
+{
|
||||
+ if (flag & TOKEN_REL) {
|
||||
+ int32 incr = flag & 0x3f;
|
||||
+ if (rx_token > MAX_TOKEN_INDEX - incr)
|
||||
+ invalid_compressed_token();
|
||||
+ rx_token += incr;
|
||||
+ flag >>= 6;
|
||||
+ } else {
|
||||
+ rx_token = read_int(f);
|
||||
+ if (rx_token < 0 || rx_token > MAX_TOKEN_INDEX)
|
||||
+ invalid_compressed_token();
|
||||
+ }
|
||||
+
|
||||
+ if (flag & 1) {
|
||||
+ rx_run = read_byte(f);
|
||||
+ rx_run += read_byte(f) << 8;
|
||||
+ if (rx_run <= 0 || rx_token > MAX_TOKEN_INDEX - rx_run)
|
||||
+ invalid_compressed_token();
|
||||
+ recv_state = r_running;
|
||||
+ }
|
||||
+
|
||||
+ return -1 - rx_token;
|
||||
+}
|
||||
+
|
||||
+static int32 recv_compressed_token_run(void)
|
||||
+{
|
||||
+ if (rx_run <= 0 || rx_token >= MAX_TOKEN_INDEX)
|
||||
+ invalid_compressed_token();
|
||||
+ ++rx_token;
|
||||
+ if (--rx_run == 0)
|
||||
+ recv_state = r_idle;
|
||||
+ return -1 - rx_token;
|
||||
+}
|
||||
+
|
||||
/* Receive a deflated token and inflate it */
|
||||
static int32 recv_deflated_token(int f, char **data)
|
||||
{
|
||||
@@ -581,17 +632,7 @@ static int32 recv_deflated_token(int f, char **data)
|
||||
}
|
||||
|
||||
/* here we have a token of some kind */
|
||||
- if (flag & TOKEN_REL) {
|
||||
- rx_token += flag & 0x3f;
|
||||
- flag >>= 6;
|
||||
- } else
|
||||
- rx_token = read_int(f);
|
||||
- if (flag & 1) {
|
||||
- rx_run = read_byte(f);
|
||||
- rx_run += read_byte(f) << 8;
|
||||
- recv_state = r_running;
|
||||
- }
|
||||
- return -1 - rx_token;
|
||||
+ return recv_compressed_token_num(f, flag);
|
||||
|
||||
case r_inflating:
|
||||
rx_strm.next_out = (Bytef *)dbuf;
|
||||
@@ -611,10 +652,7 @@ static int32 recv_deflated_token(int f, char **data)
|
||||
break;
|
||||
|
||||
case r_running:
|
||||
- ++rx_token;
|
||||
- if (--rx_run == 0)
|
||||
- recv_state = r_idle;
|
||||
- return -1 - rx_token;
|
||||
+ return recv_compressed_token_run();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -823,17 +861,7 @@ static int32 recv_zstd_token(int f, char **data)
|
||||
return 0;
|
||||
}
|
||||
/* here we have a token of some kind */
|
||||
- if (flag & TOKEN_REL) {
|
||||
- rx_token += flag & 0x3f;
|
||||
- flag >>= 6;
|
||||
- } else
|
||||
- rx_token = read_int(f);
|
||||
- if (flag & 1) {
|
||||
- rx_run = read_byte(f);
|
||||
- rx_run += read_byte(f) << 8;
|
||||
- recv_state = r_running;
|
||||
- }
|
||||
- return -1 - rx_token;
|
||||
+ return recv_compressed_token_num(f, flag);
|
||||
|
||||
case r_inflated: /* zstd doesn't get into this state */
|
||||
break;
|
||||
@@ -864,10 +892,7 @@ static int32 recv_zstd_token(int f, char **data)
|
||||
break;
|
||||
|
||||
case r_running:
|
||||
- ++rx_token;
|
||||
- if (--rx_run == 0)
|
||||
- recv_state = r_idle;
|
||||
- return -1 - rx_token;
|
||||
+ return recv_compressed_token_run();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -987,17 +1012,7 @@ static int32 recv_compressed_token(int f, char **data)
|
||||
}
|
||||
|
||||
/* here we have a token of some kind */
|
||||
- if (flag & TOKEN_REL) {
|
||||
- rx_token += flag & 0x3f;
|
||||
- flag >>= 6;
|
||||
- } else
|
||||
- rx_token = read_int(f);
|
||||
- if (flag & 1) {
|
||||
- rx_run = read_byte(f);
|
||||
- rx_run += read_byte(f) << 8;
|
||||
- recv_state = r_running;
|
||||
- }
|
||||
- return -1 - rx_token;
|
||||
+ return recv_compressed_token_num(f, flag);
|
||||
|
||||
case r_inflating:
|
||||
avail_out = LZ4_decompress_safe(next_in, dbuf, avail_in, size);
|
||||
@@ -1013,10 +1028,7 @@ static int32 recv_compressed_token(int f, char **data)
|
||||
break;
|
||||
|
||||
case r_running:
|
||||
- ++rx_token;
|
||||
- if (--rx_run == 0)
|
||||
- recv_state = r_idle;
|
||||
- return -1 - rx_token;
|
||||
+ return recv_compressed_token_run();
|
||||
}
|
||||
}
|
||||
}
|
||||
--
|
||||
2.52.0
|
||||
|
||||
@ -10,7 +10,7 @@
|
||||
Summary: A program for synchronizing files over a network
|
||||
Name: rsync
|
||||
Version: 3.2.5
|
||||
Release: 7%{?dist}
|
||||
Release: 7%{?dist}.2
|
||||
URL: https://rsync.samba.org/
|
||||
|
||||
Source0: https://download.samba.org/pub/rsync/src/rsync-%{version}%{?prerelease}.tar.gz
|
||||
@ -37,28 +37,37 @@ Provides: bundled(zlib) = 1.2.8
|
||||
License: GPLv3+
|
||||
|
||||
#Added due to rhbz#1873975 - default-acls test fail on s390x due to libacl
|
||||
Patch1: rsync-3.2.2-runtests.patch
|
||||
Patch1: rsync-3.2.2-runtests.patch
|
||||
#commonmark would be needed to generate manpage, so we simply copy it
|
||||
Patch2: rsync-3.2.5-rrsync-man.patch
|
||||
Patch2: rsync-3.2.5-rrsync-man.patch
|
||||
#A couple of fixes for the new filtering code
|
||||
Patch3: rsync-3.2.3-filtering-rules.patch
|
||||
Patch4: rsync-3.2.5-cve-2024-12085.patch
|
||||
Patch5: rsync-3.2.5-cve-2024-12087.patch
|
||||
Patch6: rsync-3.2.5-cve-2024-12088.patch
|
||||
Patch7: rsync-3.2.5-cve-2024-12747.patch
|
||||
Patch3: rsync-3.2.3-filtering-rules.patch
|
||||
Patch4: rsync-3.2.5-cve-2024-12085.patch
|
||||
Patch5: rsync-3.2.5-cve-2024-12087.patch
|
||||
Patch6: rsync-3.2.5-cve-2024-12088.patch
|
||||
Patch7: rsync-3.2.5-cve-2024-12747.patch
|
||||
# This is here for RHEL9 lifetime to avoid changes in defaults.
|
||||
# From RHEL10 this will have to be documented as a different
|
||||
# behaviour for compression.
|
||||
Patch8: rsync-3.2.5-default-compression.patch
|
||||
Patch9: rsync-3.2.5-ssh-askpass.patch
|
||||
Patch10: rsync-3.2.5-cve-2025-10158.patch
|
||||
# https://github.com/RsyncProject/rsync/commit/bb0a8118c2d2ab01140bac5e4e327e5e1ef90c9c
|
||||
Patch11: rsync-3.2.5-cve-2026-41035.patch
|
||||
# Fix for CVE-2024-12086 has three parts:
|
||||
# https://github.com/RsyncProject/rsync/commit/b4a27ca and
|
||||
# https://github.com/RsyncProject/rsync/commit/c35e283
|
||||
# These need to be followed by https://github.com/RsyncProject/rsync/commit/4fa7156
|
||||
Patch12: rsync-3.2.5-cve-2024-12086.patch
|
||||
Patch8: rsync-3.2.5-default-compression.patch
|
||||
Patch9: rsync-3.2.5-ssh-askpass.patch
|
||||
Patch10: rsync-3.2.5-fix-cve-2025-10158.patch
|
||||
# https://github.com/RsyncProject/rsync/commit/866dd713
|
||||
# https://github.com/RsyncProject/rsync/commit/1a5ad81a
|
||||
# https://github.com/RsyncProject/rsync/commit/99b36291
|
||||
# https://github.com/RsyncProject/rsync/commit/24852cda
|
||||
# https://github.com/RsyncProject/rsync/commit/d22b6bc7
|
||||
# https://github.com/RsyncProject/rsync/commit/39b3074a
|
||||
# https://github.com/RsyncProject/rsync/commit/a277a06b
|
||||
# https://github.com/RsyncProject/rsync/commit/7c8a647c
|
||||
Patch11: rsync-3.2.5-fix-cve-2026-29518.patch
|
||||
# https://github.com/RsyncProject/rsync/commit/f6b39cca
|
||||
# https://github.com/RsyncProject/rsync/commit/5ce33659
|
||||
# https://github.com/RsyncProject/rsync/commit/3526884f
|
||||
# https://github.com/RsyncProject/rsync/commit/7192db98
|
||||
Patch12: rsync-3.2.5-fix-cve-2026-29518-regressions.patch
|
||||
# https://github.com/RsyncProject/rsync/commit/901041dd
|
||||
Patch13: rsync-3.2.5-fix-cve-2026-43618.patch
|
||||
|
||||
%description
|
||||
Rsync uses a reliable algorithm to bring remote and host files into
|
||||
@ -98,18 +107,19 @@ may be used to setup a restricted rsync users via ssh logins.
|
||||
%setup -q -b 1
|
||||
%endif
|
||||
|
||||
%patch 1 -p1 -b .runtests
|
||||
%patch 2 -p1 -b .rrsync-man
|
||||
%patch 3 -p1 -b .filtering-rules
|
||||
%patch 4 -p1 -b .cve-2024-12085
|
||||
%patch 5 -p1 -b .cve-2024-12087
|
||||
%patch 6 -p1 -b .cve-2024-12088
|
||||
%patch 7 -p1 -b .cve-2024-12747
|
||||
%patch 8 -p1 -b .default-compression
|
||||
%patch 9 -p1 -b .ssh-askpass
|
||||
%patch 1 -p1 -b .runtests
|
||||
%patch 2 -p1 -b .rrsync-man
|
||||
%patch 3 -p1 -b .filtering-rules
|
||||
%patch 4 -p1 -b .cve-2024-12085
|
||||
%patch 5 -p1 -b .cve-2024-12087
|
||||
%patch 6 -p1 -b .cve-2024-12088
|
||||
%patch 7 -p1 -b .cve-2024-12747
|
||||
%patch 8 -p1 -b .default-compression
|
||||
%patch 9 -p1 -b .ssh-askpass
|
||||
%patch 10 -p1 -b .cve-2025-10158
|
||||
%patch 11 -p1 -b .cve-2026-41035
|
||||
%patch 12 -p1 -b .cve-2024-12086
|
||||
%patch 11 -p1 -b .cve-2026-29518
|
||||
%patch 12 -p1 -b .cve-2026-29518-regressions
|
||||
%patch 13 -p1 -b .cve-2026-43618
|
||||
|
||||
%build
|
||||
%configure --disable-xxhash --with-rrsync
|
||||
@ -160,14 +170,13 @@ install -D -m644 %{SOURCE6} $RPM_BUILD_ROOT/%{_unitdir}/rsyncd@.service
|
||||
%systemd_postun_with_restart rsyncd.service
|
||||
|
||||
%changelog
|
||||
* Mon May 11 2026 Michal Ruprich <mruprich@redhat.com> - 3.2.5-7
|
||||
- Resolves: RHEL-173468 - CVE-2024-12086 rsync server leaks arbitrary client files
|
||||
* Mon Jun 15 2026 Michal Ruprich <mruprich@redhat.com> - 3.2.5-7.2
|
||||
- Fix integer overflow in compressed-token decoding (CVE-2026-43618)
|
||||
- Resolves: RHEL-174932
|
||||
|
||||
* Mon May 04 2026 Michal Ruprich <mruprich@redhat.com> - 3.2.5-6
|
||||
- Resolves: RHEL-169151 - CVE-2026-41035 - Use-after-free vulnerability in extended attribute handling
|
||||
|
||||
* Tue Apr 07 2026 Michal Ruprich <mruprich@redhat.com> - 3.2.5-5
|
||||
- Resolves: RHEL-152536 - CVE-2025-10158 Out of bounds array access via negative index
|
||||
* Thu May 28 2026 RHEL Packaging Agent <redhat-ymir-agent@redhat.com> - 3.2.5-7.1
|
||||
- Fix TOCTOU symlink race in daemon no-chroot mode (CVE-2026-29518)
|
||||
- Resolves: RHEL-174952
|
||||
|
||||
* Thu Oct 09 2025 Michal Ruprich <mruprich@redhat.com> - 3.2.5-4
|
||||
- Resolves: RHEL-104404 - Do not clear DISPLAY unconditionally
|
||||
|
||||
Loading…
Reference in New Issue
Block a user