forked from rpms/rsync
import UBI rsync-3.1.3-27.el8_10
This commit is contained in:
parent
0d804b03ec
commit
2c397100cd
610
SOURCES/rsync-3.1.3-fix-cve-2026-29518-regressions.patch
Normal file
610
SOURCES/rsync-3.1.3-fix-cve-2026-29518-regressions.patch
Normal 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
|
||||
|
||||
3990
SOURCES/rsync-3.1.3-fix-cve-2026-29518.patch
Normal file
3990
SOURCES/rsync-3.1.3-fix-cve-2026-29518.patch
Normal file
File diff suppressed because it is too large
Load Diff
174
SOURCES/rsync-3.1.3-fix-cve-2026-43618.patch
Normal file
174
SOURCES/rsync-3.1.3-fix-cve-2026-43618.patch
Normal 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
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user