1
0
forked from rpms/rsync

Compare commits

...

6 Commits
c8 ... c8

Author SHA1 Message Date
AlmaLinux RelEng Bot
2c397100cd import UBI rsync-3.1.3-27.el8_10 2026-06-16 13:44:59 -04:00
AlmaLinux RelEng Bot
0d804b03ec import CS git rsync-3.1.3-25.el8_10 2026-05-14 05:35:43 -04:00
AlmaLinux RelEng Bot
6b53040a39 import CS git rsync-3.1.3-24.el8_10 2026-04-02 07:24:49 -04:00
340d7e65e4 import UBI rsync-3.1.3-23.el8_10 2025-06-03 01:54:48 +00:00
ac71ff31a9 import UBI rsync-3.1.3-21.el8_10 2025-03-11 09:32:39 +00:00
bcc2fd902a import UBI rsync-3.1.3-20.el8_10 2025-01-15 07:22:42 +00:00
12 changed files with 5404 additions and 1 deletions

View File

@ -0,0 +1,14 @@
diff --git a/match.c b/match.c
index 36e78ed..dfd6af2 100644
--- a/match.c
+++ b/match.c
@@ -147,6 +147,9 @@ static void hash_search(int f,struct sum_struct *s,
int more;
schar *map;
+ // prevent possible memory leaks
+ memset(sum2, 0, sizeof sum2);
+
/* want_i is used to encourage adjacent matches, allowing the RLL
* coding of the output to work more efficiently. */
want_i = 0;

View File

@ -0,0 +1,36 @@
diff --git a/flist.c b/flist.c
index 464d556..087f9da 100644
--- a/flist.c
+++ b/flist.c
@@ -2584,6 +2584,19 @@ struct file_list *recv_file_list(int f, int dir_ndx)
init_hard_links();
#endif
+ if (inc_recurse && dir_ndx >= 0) {
+ if (dir_ndx >= dir_flist->used) {
+ rprintf(FERROR_XFER, "rsync: refusing invalid dir_ndx %u >= %u\n", dir_ndx, dir_flist->used);
+ exit_cleanup(RERR_PROTOCOL);
+ }
+ struct file_struct *file = dir_flist->files[dir_ndx];
+ if (file->flags & FLAG_GOT_DIR_FLIST) {
+ rprintf(FERROR_XFER, "rsync: refusing malicious duplicate flist for dir %d\n", dir_ndx);
+ exit_cleanup(RERR_PROTOCOL);
+ }
+ file->flags |= FLAG_GOT_DIR_FLIST;
+ }
+
flist = flist_new(0, "recv_file_list");
if (inc_recurse) {
diff --git a/rsync.h b/rsync.h
index b357dad..bc9abac 100644
--- a/rsync.h
+++ b/rsync.h
@@ -83,6 +83,7 @@
#define FLAG_SKIP_GROUP (1<<10) /* receiver/generator */
#define FLAG_TIME_FAILED (1<<11)/* generator */
#define FLAG_MOD_NSEC (1<<12) /* sender/receiver/generator */
+#define FLAG_GOT_DIR_FLIST (1<<13)/* sender/receiver/generator - dir_flist only */
/* These flags are passed to functions but not stored. */

View File

@ -0,0 +1,57 @@
diff --git a/testsuite/unsafe-byname.test b/testsuite/unsafe-byname.test
index 75e7201..d2e318e 100644
--- a/testsuite/unsafe-byname.test
+++ b/testsuite/unsafe-byname.test
@@ -40,7 +40,7 @@ test_unsafe ..//../dest from/dir unsafe
test_unsafe .. from/file safe
test_unsafe ../.. from/file unsafe
test_unsafe ..//.. from//file unsafe
-test_unsafe dir/.. from safe
+test_unsafe dir/.. from unsafe
test_unsafe dir/../.. from unsafe
test_unsafe dir/..//.. from unsafe
diff --git a/util.c b/util.c
index da50ff1..f260d39 100644
--- a/util.c
+++ b/util.c
@@ -1318,7 +1318,14 @@ int handle_partial_dir(const char *fname, int create)
*
* "src" is the top source directory currently applicable at the level
* of the referenced symlink. This is usually the symlink's full path
- * (including its name), as referenced from the root of the transfer. */
+ * (including its name), as referenced from the root of the transfer.
+ *
+ * NOTE: this also rejects dest names with a .. component in other
+ * than the first component of the name ie. it rejects names such as
+ * a/b/../x/y. This needs to be done as the leading subpaths 'a' or
+ * 'b' could later be replaced with symlinks such as a link to '.'
+ * resulting in the link being transferred now becoming unsafe
+ */
int unsafe_symlink(const char *dest, const char *src)
{
const char *name, *slash;
@@ -1328,6 +1335,23 @@ int unsafe_symlink(const char *dest, const char *src)
if (!dest || !*dest || *dest == '/')
return 1;
+ // reject destinations with /../ in the name other than at the start of the name
+ const char *dest2 = dest;
+ while (strncmp(dest2, "../", 3) == 0) {
+ dest2 += 3;
+ while (*dest2 == '/') {
+ // allow for ..//..///../foo
+ dest2++;
+ }
+ }
+ if (strstr(dest2, "/../"))
+ return 1;
+
+ // reject if the destination ends in /..
+ const size_t dlen = strlen(dest);
+ if (dlen > 3 && strcmp(&dest[dlen-3], "/..") == 0)
+ return 1;
+
/* find out what our safety margin is */
for (name = src; (slash = strchr(name, '/')) != 0; name = slash+1) {
/* ".." segment starts the count over. "." segment is ignored. */

View File

@ -0,0 +1,141 @@
diff --git a/checksum.c b/checksum.c
index cb21882..66e8089 100644
--- a/checksum.c
+++ b/checksum.c
@@ -406,7 +406,7 @@ void file_checksum(const char *fname, const STRUCT_STAT *st_p, char *sum)
memset(sum, 0, MAX_DIGEST_LEN);
- fd = do_open(fname, O_RDONLY, 0);
+ fd = do_open_checklinks(fname);
if (fd == -1)
return;
diff --git a/generator.c b/generator.c
index 110db28..3f13bb9 100644
--- a/generator.c
+++ b/generator.c
@@ -1867,7 +1867,7 @@ static void recv_generator(char *fname, struct file_struct *file, int ndx,
}
/* open the file */
- if ((fd = do_open(fnamecmp, O_RDONLY, 0)) < 0) {
+ if ((fd = do_open_checklinks(fnamecmp)) < 0) {
rsyserr(FERROR, errno, "failed to open %s, continuing",
full_fname(fnamecmp));
pretend_missing:
diff --git a/receiver.c b/receiver.c
index 8031b8f..edfbb21 100644
--- a/receiver.c
+++ b/receiver.c
@@ -775,7 +775,7 @@ int recv_files(int f_in, int f_out, char *local_name)
if (fd1 == -1 && protocol_version < 29) {
if (fnamecmp != fname) {
fnamecmp = fname;
- fd1 = do_open(fnamecmp, O_RDONLY, 0);
+ fd1 = do_open_nofollow(fnamecmp, O_RDONLY);
}
if (fd1 == -1 && basis_dir[0]) {
diff --git a/sender.c b/sender.c
index 2bbff2f..a4d46c3 100644
--- a/sender.c
+++ b/sender.c
@@ -350,7 +350,7 @@ void send_files(int f_in, int f_out)
exit_cleanup(RERR_PROTOCOL);
}
- fd = do_open(fname, O_RDONLY, 0);
+ fd = do_open_checklinks(fname);
if (fd == -1) {
if (errno == ENOENT) {
enum logcode c = am_daemon
diff --git a/syscall.c b/syscall.c
index 47c5ea5..c55ae5f 100644
--- a/syscall.c
+++ b/syscall.c
@@ -45,6 +45,8 @@ extern int preallocate_files;
extern int preallocate_files;
extern int preserve_perms;
extern int preserve_executability;
+extern int copy_links;
+extern int copy_unsafe_links;
#ifndef S_BLKSIZE
# if defined hpux || defined __hpux__ || defined __hpux
@@ -575,3 +575,21 @@ int do_open_nofollow(const char *pathname, int flags)
return fd;
}
+
+/*
+ varient of do_open/do_open_nofollow which does do_open() if the
+ copy_links or copy_unsafe_links options are set and does
+ do_open_nofollow() otherwise
+
+ This is used to prevent a race condition where an attacker could be
+ switching a file between being a symlink and being a normal file
+
+ The open is always done with O_RDONLY flags
+ */
+int do_open_checklinks(const char *pathname)
+{
+ if (copy_links || copy_unsafe_links) {
+ return do_open(pathname, O_RDONLY, 0);
+ }
+ return do_open_nofollow(pathname, O_RDONLY);
+}
diff --git a/t_unsafe.c b/t_unsafe.c
index 010cac5..e10619a 100644
--- a/t_unsafe.c
+++ b/t_unsafe.c
@@ -28,6 +28,9 @@ int am_root = 0;
int human_readable = 0;
int preserve_perms = 0;
int preserve_executability = 0;
+int copy_links = 0;
+int copy_unsafe_links = 0;
+
short info_levels[COUNT_INFO], debug_levels[COUNT_DEBUG];
int
diff --git a/tls.c b/tls.c
index e6b0708..858f8f1 100644
--- a/tls.c
+++ b/tls.c
@@ -49,6 +49,9 @@ int list_only = 0;
int preserve_executability = 0;
int preallocate_files = 0;
int inplace = 0;
+int safe_symlinks = 0;
+int copy_links = 0;
+int copy_unsafe_links = 0;
#ifdef SUPPORT_XATTRS
diff --git a/trimslash.c b/trimslash.c
index 1ec928c..f2774cd 100644
--- a/trimslash.c
+++ b/trimslash.c
@@ -26,6 +26,8 @@ int am_root = 0;
int preserve_executability = 0;
int preallocate_files = 0;
int inplace = 0;
+int copy_links = 0;
+int copy_unsafe_links = 0;
int
main(int argc, char **argv)
diff --git a/util.c b/util.c
index f260d39..d84bc41 100644
--- a/util.c
+++ b/util.c
@@ -365,7 +365,7 @@ int copy_file(const char *source, const char *dest, int tmpfilefd, mode_t mode)
int len; /* Number of bytes read into `buf'. */
OFF_T prealloc_len = 0, offset = 0;
- if ((ifd = do_open(source, O_RDONLY, 0)) < 0) {
+ if ((ifd = do_open_nofollow(source, O_RDONLY)) < 0) {
int save_errno = errno;
rsyserr(FERROR_XFER, errno, "open %s", full_fname(source));
errno = save_errno;

View File

@ -0,0 +1,27 @@
From 797e17fc4a6f15e3b1756538a9f812b63942686f Mon Sep 17 00:00:00 2001
From: Andrew Tridgell <andrew@tridgell.net>
Date: Sat, 23 Aug 2025 17:26:53 +1000
Subject: [PATCH] fixed an invalid access to files array
this was found by Calum Hutton from Rapid7. It is a real bug, but
analysis shows it can't be leverged into an exploit. Worth fixing
though.
Many thanks to Calum and Rapid7 for finding and reporting this
---
sender.c | 2 ++
1 file changed, 2 insertions(+)
diff --git a/sender.c b/sender.c
index a4d46c39e..b1588b701 100644
--- a/sender.c
+++ b/sender.c
@@ -262,6 +262,8 @@ void send_files(int f_in, int f_out)
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];
if (F_PATHNAME(file)) {

View File

@ -0,0 +1,54 @@
diff --git a/zlib/inftrees.c b/zlib/inftrees.c
index 44d89cf2..571e8100 100644
--- a/zlib/inftrees.c
+++ b/zlib/inftrees.c
@@ -54,7 +54,7 @@ unsigned short FAR *work;
code FAR *next; /* next available space in table */
const unsigned short FAR *base; /* base value table to use */
const unsigned short FAR *extra; /* extra bits table to use */
- int end; /* use base and extra for symbol > end */
+ unsigned match; /* use base and extra for symbol >= match */
unsigned short count[MAXBITS+1]; /* number of codes of each length */
unsigned short offs[MAXBITS+1]; /* offsets in table for each length */
static const unsigned short lbase[31] = { /* Length codes 257..285 base */
@@ -181,19 +181,17 @@ unsigned short FAR *work;
switch (type) {
case CODES:
base = extra = work; /* dummy value--not used */
- end = 19;
+ match = 20;
break;
case LENS:
base = lbase;
- base -= 257;
extra = lext;
- extra -= 257;
- end = 256;
+ match = 257;
break;
default: /* DISTS */
base = dbase;
extra = dext;
- end = -1;
+ match = 0;
}
/* initialize state for loop */
@@ -216,13 +214,13 @@ unsigned short FAR *work;
for (;;) {
/* create table entry */
here.bits = (unsigned char)(len - drop);
- if ((int)(work[sym]) < end) {
+ if (work[sym] + 1u < match) {
here.op = (unsigned char)0;
here.val = work[sym];
}
- else if ((int)(work[sym]) > end) {
- here.op = (unsigned char)(extra[work[sym]]);
- here.val = base[work[sym]];
+ else if (work[sym] >= match) {
+ here.op = (unsigned char)(extra[work[sym] - match]);
+ here.val = base[work[sym] - match];
}
else {
here.op = (unsigned char)(32 + 64); /* end of block */

View File

@ -0,0 +1,15 @@
diff --git a/xattrs.c b/xattrs.c
index f732fb15..b1b4217e 100644
--- a/xattrs.c
+++ b/xattrs.c
@@ -917,8 +917,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 */

View File

@ -0,0 +1,610 @@
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, fname, O_WRONLY|O_CREAT, 0600);
+ fd2 = secure_basis_open(NULL, fname, O_WRONLY|O_CREAT, 0600);
else
fd2 = do_open(fname, O_WRONLY|O_CREAT, 0600);
if (fd2 == -1) {
--
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)
/* pre-29 allowed only one alternate basis */
basedir = basis_dir[0];
fnamecmp = fname;
- 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/util.c b/util.c
index 36c1b68c..12361057 100644
--- a/util.c
+++ b/util.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

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,174 @@
From 9ec57dd61762175a5fef11b66f36cbe6bd451178 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 | 68 ++++++++++++++++++++++++++++++++++++++++++------------
2 files changed, 63 insertions(+), 16 deletions(-)
diff --git a/receiver.c b/receiver.c
index 6044836..59f9759 100644
--- a/receiver.c
+++ b/receiver.c
@@ -305,7 +305,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);
@@ -313,6 +318,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 f1299ee..fb722a5 100644
--- a/token.c
+++ b/token.c
@@ -228,6 +228,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;
}
@@ -441,9 +449,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)
{
@@ -535,17 +586,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;
@@ -565,10 +606,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();
}
}
}
--
2.52.0

View File

@ -0,0 +1,218 @@
diff --git a/exclude.c b/exclude.c
index d36a105e..da25661b 100644
--- a/exclude.c
+++ b/exclude.c
@@ -33,18 +33,15 @@ extern int recurse;
extern int local_server;
extern int prune_empty_dirs;
extern int ignore_perishable;
-extern int old_style_args;
extern int relative_paths;
extern int delete_mode;
extern int delete_excluded;
extern int cvs_exclude;
extern int sanitize_paths;
extern int protocol_version;
-extern int read_batch;
-extern int list_only;
+extern int trust_sender_args;
extern int module_id;
-extern char *filesfrom_host;
extern char curr_dir[MAXPATHLEN];
extern unsigned int curr_dir_len;
extern unsigned int module_dirlen;
@@ -55,6 +52,7 @@ filter_rule_list daemon_filter_list = { .debug_type = " [daemon]" };
filter_rule_list implied_filter_list = { .debug_type = " [implied]" };
int saw_xattr_filter = 0;
+int trust_sender_args = 0;
int trust_sender_filter = 0;
/* Need room enough for ":MODS " prefix plus some room to grow. */
@@ -377,7 +375,7 @@ void add_implied_include(const char *arg, int skip_daemon_module)
int slash_cnt = 0;
const char *cp;
char *p;
- if (am_server || old_style_args || list_only || read_batch || filesfrom_host != NULL)
+ if (trust_sender_args)
return;
if (partial_string_len) {
arg_len = strlen(arg);
diff --git a/main.c b/main.c
index 6721ceb7..9ebfbea7 100644
--- a/main.c
+++ b/main.c
@@ -89,7 +89,6 @@ extern int backup_dir_len;
extern BOOL shutting_down;
extern int backup_dir_len;
extern int basis_dir_cnt;
-extern int trust_sender_filter;
extern struct stats stats;
extern char *stdout_format;
extern char *logfile_format;
@@ -636,7 +635,6 @@ static pid_t do_cmd(char *cmd, char *machine, char *user, char **remote_argv, in
#ifdef ICONV_CONST
setup_iconv();
#endif
- trust_sender_filter = 1;
} else if (local_server) {
/* If the user didn't request --[no-]whole-file, force
* it on, but only if we're not batch processing. */
diff --git a/options.c b/options.c
index e7a9fcae..4feeb7e0 100644
--- a/options.c
+++ b/options.c
@@ -27,6 +27,8 @@
extern int local_server;
extern int sanitize_paths;
extern int daemon_over_rsh;
+extern int trust_sender_args;
+extern int trust_sender_filter;
extern unsigned int module_dirlen;
extern filter_rule_list filter_list;
extern filter_rule_list daemon_filter_list;
@@ -64,6 +66,7 @@ int preserve_atimes = 0;
static int daemon_opt; /* sets am_daemon after option error-reporting */
static int omit_dir_times = 0;
static int omit_link_times = 0;
+int trust_sender = 0;
static int F_option_cnt = 0;
static int modify_window_set;
static int itemize_changes = 0;
@@ -788,6 +791,7 @@ static struct poptOption long_options[] = {
{"protect-args", 's', POPT_ARG_VAL, &protect_args, 1, 0, 0},
{"no-protect-args", 0, POPT_ARG_VAL, &protect_args, 0, 0, 0},
{"no-s", 0, POPT_ARG_VAL, &protect_args, 0, 0, 0},
+ {"trust-sender", 0, POPT_ARG_VAL, &trust_sender, 1, 0, 0},
{"numeric-ids", 0, POPT_ARG_VAL, &numeric_ids, 1, 0, 0 },
{"no-numeric-ids", 0, POPT_ARG_VAL, &numeric_ids, 0, 0, 0 },
{"usermap", 0, POPT_ARG_STRING, 0, OPT_USERMAP, 0, 0 },
@@ -2465,6 +2469,11 @@ int parse_arguments(int *argc_p, const char ***argv_p)
}
}
+ if (trust_sender || am_server || read_batch)
+ trust_sender_args = trust_sender_filter = 1;
+ else if (old_style_args || filesfrom_host != NULL)
+ trust_sender_args = 1;
+
am_starting_up = 0;
return 1;
@@ -2438,9 +2438,7 @@ char *safe_arg(const char *opt, const char *arg)
char *ret;
if (!protect_args && old_style_args < 2 && (!old_style_args || (!is_filename_arg && opt != SPLIT_ARG_WHEN_OLD))) {
const char *f;
- if (!old_style_args && *arg == '~'
- && ((relative_paths && !strstr(arg, "/./"))
- || !strchr(arg, '/'))) {
+ if (!trust_sender_args && *arg == '~' && (relative_paths || !strchr(arg, '/'))) {
extras++;
escape_leading_tilde = 1;
}
diff --git a/rsync.1.old b/rsync.1
index 839f5ad..6882cf5 100644
--- a/rsync.1.old
+++ b/rsync.1
@@ -182,9 +182,39 @@ particular rsync daemon by leaving off the module name:
\f(CWrsync somehost.mydomain.com::\fP
.RE
-.PP
-See the following section for more details.
-.PP
+.SH "MULTI-HOST SECURITY"
+
+.PP
+Rsync takes steps to ensure that the file requests that are shared in a
+transfer are protected against various security issues. Most of the potential
+problems arise on the receiving side where rsync takes steps to ensure that the
+list of files being transferred remains within the bounds of what was
+requested.
+.PP
+Toward this end, rsync 3.1.2 and later have aborted when a file list contains
+an absolute or relative path that tries to escape out of the top of the
+transfer. Also, beginning with version 3.2.5, rsync does two more safety
+checks of the file list to (1) ensure that no extra source arguments were added
+into the transfer other than those that the client requested and (2) ensure
+that the file list obeys the exclude rules that we sent to the sender.
+.PP
+For those that don't yet have a 3.2.5 client rsync, it is safest to do a copy
+into a dedicated destination directory for the remote files rather than
+requesting the remote content get mixed in with other local content. For
+example, doing an rsync copy into your home directory is potentially unsafe on
+an older rsync if the remote rsync is being controlled by a bad actor:
+.PP
+.RS
+\f(CWrsync \-aiv host:dir1 ~\fP
+.RE
+.PP
+A safer command would be:
+.RS
+\f(CWrsync \-aiv host:dir1 ~/host-files\fP
+.RE
+.PP
+See the \fB\-\-trust\-sender\fP option for additional details.
+
.SH "ADVANCED USAGE"
.PP
@@ -519,6 +549,7 @@ to the detailed description below for a complete description.
\-0, \-\-from0 all *from/filter files are delimited by 0s
\-\-old\-args disable the modern arg-protection idiom
\-s, \-\-protect\-args no space\-splitting; wildcard chars only
+ \-\-trust\-sender trust the remote sender's file list
\-\-address=ADDRESS bind address for outgoing socket to daemon
\-\-port=PORT specify double\-colon alternate port number
\-\-sockopts=OPTIONS specify custom TCP options
@@ -2119,6 +2150,49 @@ This option conflicts with the \fB\-\-old\-args\fP option.
Note that this option is incompatible with the use of the restricted rsync
script (`rrsync`) since it hides options from the script's inspection.
.IP
+.IP "\fB\-\-trust\-sender\fP"
+This option disables two extra validation checks that a local client
+performs on the file list generated by a remote sender. This option should
+only be used if you trust the sender to not put something malicious in the
+file list (something that could possibly be done via a modified rsync, a
+modified shell, or some other similar manipulation).
+.IP
+Normally, the rsync client (as of version 3.2.5) runs two extra validation
+checks when pulling files from a remote rsync:
+.RS
+.IP o
+It verifies that additional arg items didn't get added at the top of the
+transfer.
+.IP o
+It verifies that none of the items in the file list are names that should
+have been excluded (if filter rules were specified).
+.RE
+.IP
+Note that various options can turn off one or both of these checks if the
+option interferes with the validation. For instance:
+.RS
+.IP o
+Using a per-directory filter file reads filter rules that only the server
+knows about, so the filter checking is disabled.
+.IP o
+Using the \fB\-\-old\-args\fP option allows the sender to manipulate the
+requested args, so the arg checking is disabled.
+.IP o
+Reading the files-from list from the server side means that the client
+doesn't know the arg list, so the arg checking is disabled.
+.IP o
+Using \fB\-\-read\-batch\fP disables both checks since the batch file's
+contents will have been verified when it was created.
+.RE
+.IP
+This option may help an under-powered client server if the extra pattern
+matching is slowing things down on a huge transfer. It can also be used
+work around a currently-unknown bug in the verification logic for a transfer
+from a trusted sender.
+.IP
+When using this option it is a good idea to specify a dedicated destination
+directory, as discussed in the \(dq\&MULTI-HOST SECURITY\(dq\& section.
+.IP
.IP "\fB\-T, \-\-temp\-dir=DIR\fP"
This option instructs rsync to use DIR as a
scratch directory when creating temporary copies of the files transferred

View File

@ -9,7 +9,7 @@
Summary: A program for synchronizing files over a network
Name: rsync
Version: 3.1.3
Release: 19%{?dist}.1
Release: 27%{?dist}
Group: Applications/Internet
URL: http://rsync.samba.org/
@ -42,6 +42,33 @@ Patch11: rsync-3.1.3-cve-2022-29154.patch
Patch12: rsync-3.1.3-cve-2022-37434.patch
Patch13: rsync-3.1.3-filtering-rules.patch
Patch14: rsync-3.1.3-missing-xattr-filter.patch
Patch15: rsync-3.1.3-cve-2024-12085.patch
Patch16: rsync-3.1.3-cve-2024-12087.patch
Patch17: rsync-3.1.3-cve-2024-12088.patch
Patch18: rsync-3.1.3-cve-2024-12747.patch
# a fix for CVE-2016-9840 in zlib but marked as CVE-2025-4638 for a different component
Patch19: rsync-3.1.3-cve-2025-4638.patch
Patch20: rsync-3.1.3-trust-sender.patch
Patch21: rsync-3.1.3-cve-2025-10158.patch
# https://github.com/RsyncProject/rsync/commit/bb0a8118c2d2ab01140bac5e4e327e5e1ef90c9c
Patch22: rsync-3.1.3-cve-2026-41035.patch
# https://github.com/RsyncProject/rsync/commit/1a5ad81add1004354a3d8ba841b94ffe19cd2505
# https://github.com/RsyncProject/rsync/commit/99b36291d06ca66229942c7a525a1f5566f10c85
# https://github.com/RsyncProject/rsync/commit/72d1cf1c288e5c526e906db2edafbf3d55762668
# https://github.com/RsyncProject/rsync/commit/61d987c54a472d88855c5fbef3a4c7b51696f93a
# https://github.com/RsyncProject/rsync/commit/24852cda3db38e2f2cd78a13703373c77f75f4d5
# https://github.com/RsyncProject/rsync/commit/d22b6bc7d1b1d7be9df1c0c6db1599cb7d5fd82c
# https://github.com/RsyncProject/rsync/commit/39b3074a1ab18705cd685fe0659fc958c8cd3db5
# https://github.com/RsyncProject/rsync/commit/a277a06b1017b4cf6bb0fe33d5823869ed02dfd9
Patch23: rsync-3.1.3-fix-cve-2026-29518.patch
# Backporting a couple of regression fixes
# 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
Patch24: rsync-3.1.3-fix-cve-2026-29518-regressions.patch
# https://github.com/RsyncProject/rsync/commit/c44c90e9460c666c965446a8c0957f0b9fa4c66a
Patch25: rsync-3.1.3-fix-cve-2026-43618.patch
%description
Rsync uses a reliable algorithm to bring remote and host files into
@ -94,6 +121,17 @@ patch -p1 -i patches/copy-devices.diff
%patch12 -p1 -b .cve-2022-37434
%patch13 -p1 -b .filtering-rules
%patch14 -p1 -b .xattr-filter
%patch15 -p1 -b .cve-2024-12085
%patch16 -p1 -b .cve-2024-12087
%patch17 -p1 -b .cve-2024-12088
%patch18 -p1 -b .cve-2024-12747
%patch19 -p1 -b .cve-2025-4638
%patch20 -p1 -b .trust-sender
%patch21 -p1 -b .cve-2025-10158
%patch22 -p1 -b .cve-2026-41035
%patch23 -p1 -b .cve-2026-29518
%patch24 -p1 -b .cve-2026-29518-regressions
%patch25 -p1 -b .cve-2026-43618
%build
%configure
@ -140,6 +178,35 @@ chmod -x support/*
%systemd_postun_with_restart rsyncd.service
%changelog
* Mon Jun 15 2026 Michal Ruprich <mruprich@redhat.com> - 3.1.3-27
- Integer overflow in compressed-token decoding (CVE-2026-43618)
- Resolves: RHEL-174951
* Thu May 28 2026 RHEL Packaging Agent <redhat-ymir-agent@redhat.com> - 3.1.3-26
- Resolves: RHEL-174950 - CVE-2026-29518 - TOCTOU symlink race in
non-chrooted daemon modules
* Tue May 05 2026 Michal Ruprich <mruprich@redhat.com> - 3.1.3-25
- Resolves: RHEL-169141 - CVE-2026-41035 - Use-after-free vulnerability in extended attribute handling
* Wed Mar 11 2026 Michal Ruprich <mruprich@redhat.com> - 3.1.3-24
- Resolves: RHEL-152887 - CVE-2025-10158 - Out of bounds array access via negative index
* Wed May 28 2025 Michal Ruprich <mruprich@redhat.com> - 3.1.3-23
- Resolves: RHEL-52004 - Slowness in rsync due to extra validation steps
* Mon May 26 2025 Michal Ruprich <mruprich@redhat.com> - 3.1.3-22
- Resolves: RHEL-91519 - Improper Pointer Arithmetic in pcl
* Tue Feb 04 2025 Michal Ruprich <mruprich@redhat.com> - 3.1.3-21
- Resolves: RHEL-70207 - Path traversal vulnerability in rsync
* Mon Feb 03 2025 Michal Ruprich <mruprich@redhat.com> - 3.1.3-20
- Resolves: RHEL-70207 - Path traversal vulnerability in rsync
- Resolves: RHEL-70209 - --safe-links option bypass leads to path traversal
- Resolves: RHEL-72502 - Race Condition in rsync Handling Symbolic Links
- Resolves: RHEL-70157 - Info Leak via Uninitialized Stack Contents
* Wed Nov 02 2022 Michal Ruprich <mruprich@redhat.com> - 3.1.3-19.1
- Resolves: #2139118 - rsync-daemon fail on 3.1.3