1
0
forked from rpms/rsync

import UBI rsync-3.1.3-27.el8_10

This commit is contained in:
AlmaLinux RelEng Bot 2026-06-16 12:44:59 -04:00
parent 0d804b03ec
commit 2c397100cd
4 changed files with 4803 additions and 1 deletions

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

@ -9,7 +9,7 @@
Summary: A program for synchronizing files over a network
Name: rsync
Version: 3.1.3
Release: 25%{?dist}
Release: 27%{?dist}
Group: Applications/Internet
URL: http://rsync.samba.org/
@ -52,6 +52,23 @@ 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
@ -112,6 +129,9 @@ patch -p1 -i patches/copy-devices.diff
%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
@ -158,6 +178,14 @@ 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