Version in base suite: 3.2.7-1+deb12u4 Base version: rsync_3.2.7-1+deb12u4 Target version: rsync_3.2.7-1+deb12u5 Base file: /srv/ftp-master.debian.org/ftp/pool/main/r/rsync/rsync_3.2.7-1+deb12u4.dsc Target file: /srv/ftp-master.debian.org/policy/pool/main/r/rsync/rsync_3.2.7-1+deb12u5.dsc changelog | 17 patches/2026-05-20/0001-Fix-warning-about-conflicting-lseek-lseek64-prototyp.patch | 49 patches/2026-05-20/0002-hlink-Fix-function-pointer-cast-in-qsort.patch | 30 patches/2026-05-20/0003-bool-is-a-keyword-in-C23.patch | 26 patches/2026-05-20/0004-Fix-warning-about-missing-bomb-.-prototype.patch | 36 patches/2026-05-20/0005-Some-checksum-buffer-fixes.patch | 151 patches/2026-05-20/0006-Another-cast-when-multiplying-integers.patch | 39 patches/2026-05-20/0007-prevent-information-leak-off-the-stack.patch | 27 patches/2026-05-20/0008-refuse-fuzzy-options-when-fuzzy-not-selected.patch | 37 patches/2026-05-20/0009-added-secure_relative_open.patch | 103 patches/2026-05-20/0010-receiver-use-secure_relative_open-for-basis-file.patch | 103 patches/2026-05-20/0011-disallow-.-elements-in-relpath-for-secure_relative_o.patch | 38 patches/2026-05-20/0012-Refuse-a-duplicate-dirlist.patch | 45 patches/2026-05-20/0013-range-check-dir_ndx-before-use.patch | 27 patches/2026-05-20/0014-make-safe-links-stricter.patch | 136 patches/2026-05-20/0015-fixed-symlink-race-condition-in-sender.patch | 187 + patches/2026-05-20/0016-syscall-fix-a-Y2038-bug-by-replacing-Int32x32To64-wi.patch | 29 patches/2026-05-20/0017-options.c-Fix-segv-if-poptGetContext-returns-NULL.patch | 35 patches/2026-05-20/0018-Using-a-correct-time-in-log-file.patch | 53 patches/2026-05-20/0019-configure.ac-check-for-xattr-support-both-in-libc-an.patch | 54 patches/2026-05-20/0020-util-fixed-issue-in-clean_fname.patch | 43 patches/2026-05-20/0021-testsuite-added-clean-fname-underflow-test.patch | 85 patches/2026-05-20/0022-fixed-an-invalid-access-to-files-array.patch | 30 patches/2026-05-20/0023-fix-uninitialized-buf1-in-get_checksum2-MD4-path.patch | 33 patches/2026-05-20/0024-reject-negative-token-values-in-compressed-stream-re.patch | 72 patches/2026-05-20/0025-acl-fixed-ACL-ID-mapping-for-non-root.patch | 26 patches/2026-05-20/0026-fix-uninitialized-mul_one-in-AVX2-checksum-and-add-S.patch | 205 + patches/2026-05-20/0027-Fix-glibc-2.43-constness-warnings.patch | 115 patches/2026-05-20/0028-zlib-convert-K-R-function-definitions-to-ANSI-style.patch | 1168 ++++++ patches/2026-05-20/0029-fix-signed-integer-overflow-in-proxy-protocol-v2-hea.patch | 38 patches/2026-05-20/0030-zero-all-new-memory-from-allocations.patch | 48 patches/2026-05-20/0031-xattrs-fixed-count-in-qsort.patch | 35 patches/2026-05-20/0032-call-tzset-before-chroot-to-cache-timezone-data.patch | 39 patches/2026-05-20/0033-testsuite-xattrs-ignore-SUNWattr_-in-the-Solaris-xls.patch | 45 patches/2026-05-20/0034-syscall-use-openat2-RESOLVE_BENEATH-on-Linux-for-sec.patch | 391 ++ patches/2026-05-20/0035-syscall-also-use-O_RESOLVE_BENEATH-on-FreeBSD-and-Ma.patch | 96 patches/2026-05-20/0036-testsuite-skip-symlink-dirlink-basis-on-platforms-wi.patch | 49 patches/2026-05-20/0037-syscall-clientserver-am_chrooted-and-use_secure_syml.patch | 325 + patches/2026-05-20/0038-sender-fix-read-path-TOCTOU-by-opening-from-module-r.patch | 68 patches/2026-05-20/0039-syscall-receiver-secure-receiver-side-do_chmod-again.patch | 463 ++ patches/2026-05-20/0040-util1-secure-change_dir-against-symlink-race-chdir-e.patch | 203 + patches/2026-05-20/0041-syscall-add-symlink-race-safe-do_-_at-wrappers-and-h.patch | 1775 ++++++++++ patches/2026-05-20/0042-util1-syscall-secure-copy_file-source-dest-opens-bar.patch | 565 +++ patches/2026-05-20/0043-testsuite-end-to-end-regression-test-for-chdir-symli.patch | 289 + patches/2026-05-20/0044-token-harden-compressed-token-decoding-against-integ.patch | 251 + patches/2026-05-20/0045-testsuite-cover-refuse-options-compress-for-the-daem.patch | 81 patches/2026-05-20/0046-receiver-add-parent_ndx-0-guard-mirroring-797e17f.patch | 119 patches/2026-05-20/0047-clientserver-fix-hostname-ACL-bypass-when-using-daem.patch | 192 + patches/2026-05-20/0048-defence-in-depth-bound-wire-supplied-counts-and-leng.patch | 261 + patches/2026-05-20/0049-defence-in-depth-guard-cumulative-snprintf-against-l.patch | 79 patches/2026-05-20/0050-defence-in-depth-receiver-block-index-bounds-read_de.patch | 80 patches/2026-05-20/0052-exclude-fix-crashes-with-fortified-strlcpy.patch | 51 patches/2026-05-20/0053-testsuite-use-integer-sleep-in-clean-fname-underflow.patch | 32 patches/2026-05-20/0055-popt-fix-poptDupArgv-strlcpy-size-argument.patch | 55 patches/CVE-2024-12084/0001-Some-checksum-buffer-fixes.patch | 151 patches/CVE-2024-12084/0002-Another-cast-when-multiplying-integers.patch | 39 patches/CVE-2024-12085/0001-prevent-information-leak-off-the-stack.patch | 27 patches/CVE-2024-12086/0001-refuse-fuzzy-options-when-fuzzy-not-selected.patch | 37 patches/CVE-2024-12086/0002-added-secure_relative_open.patch | 103 patches/CVE-2024-12086/0003-receiver-use-secure_relative_open-for-basis-file.patch | 103 patches/CVE-2024-12086/0004-disallow-.-elements-in-relpath-for-secure_relative_o.patch | 37 patches/CVE-2024-12087/0001-Refuse-a-duplicate-dirlist.patch | 45 patches/CVE-2024-12087/0002-range-check-dir_ndx-before-use.patch | 27 patches/CVE-2024-12088/0001-make-safe-links-stricter.patch | 136 patches/CVE-2024-12747/0001-fixed-symlink-race-condition-in-sender.patch | 187 - patches/CVE-2025-10158.patch | 26 patches/Fix_use-after-free_in_generator.patch | 31 patches/fix-aclocalm4-include-paths.patch | 16 patches/series | 67 tests/upstream-tests | 2 tests/upstream-tests-as-root | 2 71 files changed, 8701 insertions(+), 964 deletions(-) dpkg-source: warning: cannot verify inline signature for /srv/release.debian.org/tmp/tmpryabtxxf/rsync_3.2.7-1+deb12u4.dsc: no acceptable signature found dpkg-source: warning: cannot verify inline signature for /srv/release.debian.org/tmp/tmpryabtxxf/rsync_3.2.7-1+deb12u5.dsc: no acceptable signature found diff -Nru rsync-3.2.7/debian/changelog rsync-3.2.7/debian/changelog --- rsync-3.2.7/debian/changelog 2025-11-28 00:49:27.000000000 +0000 +++ rsync-3.2.7/debian/changelog 2026-05-20 06:10:17.000000000 +0000 @@ -1,3 +1,20 @@ +rsync (3.2.7-1+deb12u5) bookworm-security; urgency=high + + * Non-maintainer upload by the Security Team. + * Address several vulnerabilities + - CVE-2026-29518: Symlink-race TOCTOU in daemon (use chroot = no) + - CVE-2026-43617: Authorization bypass via hostname resolution (daemon + chroot mode) + - CVE-2026-43618: Integer overflow in compressed-token decoder (info + disclosure) + - CVE-2026-43619: Symlink-race conditions in path-based syscalls + - CVE-2026-43620: Out-of-bounds array read in receiver recv_files() + * d/t/upstream-tests: Build t_chmod_secure and t_secure_relpath + * Fix relative paths in aclocal.m4 copy upstream for + m4/{have_type,header_major_fixed,socklen_t}.m4 + + -- Salvatore Bonaccorso Wed, 20 May 2026 08:10:17 +0200 + rsync (3.2.7-1+deb12u4) bookworm; urgency=medium * Team upload. diff -Nru rsync-3.2.7/debian/patches/2026-05-20/0001-Fix-warning-about-conflicting-lseek-lseek64-prototyp.patch rsync-3.2.7/debian/patches/2026-05-20/0001-Fix-warning-about-conflicting-lseek-lseek64-prototyp.patch --- rsync-3.2.7/debian/patches/2026-05-20/0001-Fix-warning-about-conflicting-lseek-lseek64-prototyp.patch 1970-01-01 00:00:00.000000000 +0000 +++ rsync-3.2.7/debian/patches/2026-05-20/0001-Fix-warning-about-conflicting-lseek-lseek64-prototyp.patch 2026-05-20 05:47:50.000000000 +0000 @@ -0,0 +1,49 @@ +From 9585830e1e08ac84454832c6b91bf726d2b0f8d7 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Holger=20Hoffst=C3=A4tte?= +Date: Mon, 4 Sep 2023 14:07:14 +0200 +Subject: [PATCH 01/56] Fix warning about conflicting lseek/lseek64 prototypes +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +Clang rightfully complains about conflicting prototypes, as both lseek() variants +are redefined: + + syscall.c:394:10: warning: a function declaration without a prototype is deprecated + in all versions of C and is treated as a zero-parameter prototype in C2x, conflicting + with a previous declaration [-Wdeprecated-non-prototype] + off64_t lseek64(); + ^ +/usr/include/unistd.h:350:18: note: conflicting prototype is here +extern __off64_t lseek64 (int __fd, __off64_t __offset, int __whence) + ^ +1 warning generated. + +The point of the #ifdef is to build for the configured OFF_T; there is +no reason to redefine lseek/lseek64, which should have been found +via configure. + +Signed-off-by: Holger Hoffstätte +--- + syscall.c | 5 ----- + 1 file changed, 5 deletions(-) + +diff --git a/syscall.c b/syscall.c +index d92074aa..b4b0f1f1 100644 +--- a/syscall.c ++++ b/syscall.c +@@ -388,11 +388,6 @@ int do_fstat(int fd, STRUCT_STAT *st) + OFF_T do_lseek(int fd, OFF_T offset, int whence) + { + #ifdef HAVE_LSEEK64 +-#if !SIZEOF_OFF64_T +- OFF_T lseek64(); +-#else +- off64_t lseek64(); +-#endif + return lseek64(fd, offset, whence); + #else + return lseek(fd, offset, whence); +-- +2.51.0 + diff -Nru rsync-3.2.7/debian/patches/2026-05-20/0002-hlink-Fix-function-pointer-cast-in-qsort.patch rsync-3.2.7/debian/patches/2026-05-20/0002-hlink-Fix-function-pointer-cast-in-qsort.patch --- rsync-3.2.7/debian/patches/2026-05-20/0002-hlink-Fix-function-pointer-cast-in-qsort.patch 1970-01-01 00:00:00.000000000 +0000 +++ rsync-3.2.7/debian/patches/2026-05-20/0002-hlink-Fix-function-pointer-cast-in-qsort.patch 2026-05-20 05:47:50.000000000 +0000 @@ -0,0 +1,30 @@ +From f1cf29cefa91a24b3b164172b7c9b36a7845cc7a Mon Sep 17 00:00:00 2001 +From: Charalampos Mitrodimas +Date: Wed, 20 Nov 2024 14:55:50 +0200 +Subject: [PATCH 02/56] hlink: Fix function pointer cast in qsort() + +Replace unsafe generic function pointer cast with proper type cast for +qsort() comparison function. This fixes a potential type mismatch +warning without changing the behavior. + +Signed-off-by: Charalampos Mitrodimas +--- + hlink.c | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/hlink.c b/hlink.c +index 20291f26..2c14407a 100644 +--- a/hlink.c ++++ b/hlink.c +@@ -117,7 +117,7 @@ static void match_gnums(int32 *ndx_list, int ndx_count) + struct ht_int32_node *node = NULL; + int32 gnum, gnum_next; + +- qsort(ndx_list, ndx_count, sizeof ndx_list[0], (int (*)()) hlink_compare_gnum); ++ qsort(ndx_list, ndx_count, sizeof ndx_list[0], (int (*)(const void*, const void*))hlink_compare_gnum); + + for (from = 0; from < ndx_count; from++) { + file = hlink_flist->sorted[ndx_list[from]]; +-- +2.51.0 + diff -Nru rsync-3.2.7/debian/patches/2026-05-20/0003-bool-is-a-keyword-in-C23.patch rsync-3.2.7/debian/patches/2026-05-20/0003-bool-is-a-keyword-in-C23.patch --- rsync-3.2.7/debian/patches/2026-05-20/0003-bool-is-a-keyword-in-C23.patch 1970-01-01 00:00:00.000000000 +0000 +++ rsync-3.2.7/debian/patches/2026-05-20/0003-bool-is-a-keyword-in-C23.patch 2026-05-20 05:47:50.000000000 +0000 @@ -0,0 +1,26 @@ +From 292d9de105e6206584285651d5fbf546abe3a109 Mon Sep 17 00:00:00 2001 +From: Michal Ruprich +Date: Fri, 17 Jan 2025 12:37:57 +0100 +Subject: [PATCH 03/56] bool is a keyword in C23 + +--- + wildtest.c | 2 ++ + 1 file changed, 2 insertions(+) + +diff --git a/wildtest.c b/wildtest.c +index bea4cebb..482cdf17 100644 +--- a/wildtest.c ++++ b/wildtest.c +@@ -32,7 +32,9 @@ int fnmatch_errors = 0; + + int wildmatch_errors = 0; + ++#if !defined(__STDC_VERSION__) || __STDC_VERSION__ < 202311L + typedef char bool; ++#endif + + int output_iterations = 0; + int explode_mod = 0; +-- +2.51.0 + diff -Nru rsync-3.2.7/debian/patches/2026-05-20/0004-Fix-warning-about-missing-bomb-.-prototype.patch rsync-3.2.7/debian/patches/2026-05-20/0004-Fix-warning-about-missing-bomb-.-prototype.patch --- rsync-3.2.7/debian/patches/2026-05-20/0004-Fix-warning-about-missing-bomb-.-prototype.patch 1970-01-01 00:00:00.000000000 +0000 +++ rsync-3.2.7/debian/patches/2026-05-20/0004-Fix-warning-about-missing-bomb-.-prototype.patch 2026-05-20 05:47:50.000000000 +0000 @@ -0,0 +1,36 @@ +From 77b7f5e0aa546f1f343c97dcd186aa05a8f18791 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Holger=20Hoffst=C3=A4tte?= +Date: Mon, 4 Sep 2023 14:05:21 +0200 +Subject: [PATCH 04/56] Fix warning about missing bomb(..) prototype +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +Clang rightfully complains about invoking bomb(..) without a proper prototype: + lib/pool_alloc.c:171:16: warning: passing arguments to a function without a prototype + is deprecated in all versions of C and is not supported in C2x [-Wdeprecated-non-prototype] + (*pool->bomb)(bomb_msg, __FILE__, __LINE__); + ^ +1 warning generated. + +Signed-off-by: Holger Hoffstätte +--- + lib/pool_alloc.c | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/lib/pool_alloc.c b/lib/pool_alloc.c +index a1a7245f..bc1cc1a6 100644 +--- a/lib/pool_alloc.c ++++ b/lib/pool_alloc.c +@@ -9,7 +9,7 @@ struct alloc_pool + size_t size; /* extent size */ + size_t quantum; /* allocation quantum */ + struct pool_extent *extents; /* top extent is "live" */ +- void (*bomb)(); /* called if malloc fails */ ++ void (*bomb)(const char*, const char*, int); /* called if malloc fails */ + int flags; + + /* statistical data */ +-- +2.51.0 + diff -Nru rsync-3.2.7/debian/patches/2026-05-20/0005-Some-checksum-buffer-fixes.patch rsync-3.2.7/debian/patches/2026-05-20/0005-Some-checksum-buffer-fixes.patch --- rsync-3.2.7/debian/patches/2026-05-20/0005-Some-checksum-buffer-fixes.patch 1970-01-01 00:00:00.000000000 +0000 +++ rsync-3.2.7/debian/patches/2026-05-20/0005-Some-checksum-buffer-fixes.patch 2026-05-20 05:47:50.000000000 +0000 @@ -0,0 +1,151 @@ +From 147c7a0d977b0a98a078369defbc618ad7387e50 Mon Sep 17 00:00:00 2001 +From: Wayne Davison +Date: Tue, 29 Oct 2024 22:55:29 -0700 +Subject: [PATCH 05/56] Some checksum buffer fixes. + +- Put sum2_array into sum_struct to hold an array of sum2 checksums + that are each xfer_sum_len bytes. +- Remove sum2 buf from sum_buf. +- Add macro sum2_at() to access each sum2 array element. +- Throw an error if a sums header has an s2length larger than + xfer_sum_len. +--- + io.c | 3 ++- + match.c | 8 ++++---- + rsync.c | 5 ++++- + rsync.h | 4 +++- + sender.c | 4 +++- + 5 files changed, 16 insertions(+), 8 deletions(-) + +diff --git a/io.c b/io.c +index a99ac0ec..bb60eeca 100644 +--- a/io.c ++++ b/io.c +@@ -55,6 +55,7 @@ extern int read_batch; + extern int compat_flags; + extern int protect_args; + extern int checksum_seed; ++extern int xfer_sum_len; + extern int daemon_connection; + extern int protocol_version; + extern int remove_source_files; +@@ -1977,7 +1978,7 @@ void read_sum_head(int f, struct sum_struct *sum) + exit_cleanup(RERR_PROTOCOL); + } + sum->s2length = protocol_version < 27 ? csum_length : (int)read_int(f); +- if (sum->s2length < 0 || sum->s2length > MAX_DIGEST_LEN) { ++ if (sum->s2length < 0 || sum->s2length > xfer_sum_len) { + rprintf(FERROR, "Invalid checksum length %d [%s]\n", + sum->s2length, who_am_i()); + exit_cleanup(RERR_PROTOCOL); +diff --git a/match.c b/match.c +index 6243994c..94d77b87 100644 +--- a/match.c ++++ b/match.c +@@ -232,7 +232,7 @@ static void hash_search(int f,struct sum_struct *s, + done_csum2 = 1; + } + +- if (memcmp(sum2,s->sums[i].sum2,s->s2length) != 0) { ++ if (memcmp(sum2, sum2_at(s, i), s->s2length) != 0) { + false_alarms++; + continue; + } +@@ -252,7 +252,7 @@ static void hash_search(int f,struct sum_struct *s, + if (i != aligned_i) { + if (sum != s->sums[aligned_i].sum1 + || l != s->sums[aligned_i].len +- || memcmp(sum2, s->sums[aligned_i].sum2, s->s2length) != 0) ++ || memcmp(sum2, sum2_at(s, aligned_i), s->s2length) != 0) + goto check_want_i; + i = aligned_i; + } +@@ -271,7 +271,7 @@ static void hash_search(int f,struct sum_struct *s, + if (sum != s->sums[i].sum1) + goto check_want_i; + get_checksum2((char *)map, l, sum2); +- if (memcmp(sum2, s->sums[i].sum2, s->s2length) != 0) ++ if (memcmp(sum2, sum2_at(s, i), s->s2length) != 0) + goto check_want_i; + /* OK, we have a re-alignment match. Bump the offset + * forward to the new match point. */ +@@ -290,7 +290,7 @@ static void hash_search(int f,struct sum_struct *s, + && (!updating_basis_file || s->sums[want_i].offset >= offset + || s->sums[want_i].flags & SUMFLG_SAME_OFFSET) + && sum == s->sums[want_i].sum1 +- && memcmp(sum2, s->sums[want_i].sum2, s->s2length) == 0) { ++ && memcmp(sum2, sum2_at(s, want_i), s->s2length) == 0) { + /* we've found an adjacent match - the RLL coder + * will be happy */ + i = want_i; +diff --git a/rsync.c b/rsync.c +index cd288f57..b130aba5 100644 +--- a/rsync.c ++++ b/rsync.c +@@ -437,7 +437,10 @@ int read_ndx_and_attrs(int f_in, int f_out, int *iflag_ptr, uchar *type_ptr, cha + */ + void free_sums(struct sum_struct *s) + { +- if (s->sums) free(s->sums); ++ if (s->sums) { ++ free(s->sums); ++ free(s->sum2_array); ++ } + free(s); + } + +diff --git a/rsync.h b/rsync.h +index d3709fe0..8ddbe702 100644 +--- a/rsync.h ++++ b/rsync.h +@@ -958,12 +958,12 @@ struct sum_buf { + uint32 sum1; /**< simple checksum */ + int32 chain; /**< next hash-table collision */ + short flags; /**< flag bits */ +- char sum2[SUM_LENGTH]; /**< checksum */ + }; + + struct sum_struct { + OFF_T flength; /**< total file length */ + struct sum_buf *sums; /**< points to info for each chunk */ ++ char *sum2_array; /**< checksums of length xfer_sum_len */ + int32 count; /**< how many chunks */ + int32 blength; /**< block_length */ + int32 remainder; /**< flength % block_length */ +@@ -982,6 +982,8 @@ struct map_struct { + int status; /* first errno from read errors */ + }; + ++#define sum2_at(s, i) ((s)->sum2_array + ((OFF_T)(i) * xfer_sum_len)) ++ + #define NAME_IS_FILE (0) /* filter name as a file */ + #define NAME_IS_DIR (1<<0) /* filter name as a dir */ + #define NAME_IS_XATTR (1<<2) /* filter name as an xattr */ +diff --git a/sender.c b/sender.c +index 3d4f052e..ab205341 100644 +--- a/sender.c ++++ b/sender.c +@@ -31,6 +31,7 @@ extern int log_before_transfer; + extern int stdout_format_has_i; + extern int logfile_format_has_i; + extern int want_xattr_optim; ++extern int xfer_sum_len; + extern int csum_length; + extern int append_mode; + extern int copy_links; +@@ -94,10 +95,11 @@ static struct sum_struct *receive_sums(int f) + return(s); + + s->sums = new_array(struct sum_buf, s->count); ++ s->sum2_array = new_array(char, s->count * xfer_sum_len); + + for (i = 0; i < s->count; i++) { + s->sums[i].sum1 = read_int(f); +- read_buf(f, s->sums[i].sum2, s->s2length); ++ read_buf(f, sum2_at(s, i), s->s2length); + + s->sums[i].offset = offset; + s->sums[i].flags = 0; +-- +2.51.0 + diff -Nru rsync-3.2.7/debian/patches/2026-05-20/0006-Another-cast-when-multiplying-integers.patch rsync-3.2.7/debian/patches/2026-05-20/0006-Another-cast-when-multiplying-integers.patch --- rsync-3.2.7/debian/patches/2026-05-20/0006-Another-cast-when-multiplying-integers.patch 1970-01-01 00:00:00.000000000 +0000 +++ rsync-3.2.7/debian/patches/2026-05-20/0006-Another-cast-when-multiplying-integers.patch 2026-05-20 05:47:50.000000000 +0000 @@ -0,0 +1,39 @@ +From 9119310d016ba5757c9dbe664dcc844c96e64f53 Mon Sep 17 00:00:00 2001 +From: Wayne Davison +Date: Tue, 5 Nov 2024 11:01:03 -0800 +Subject: [PATCH 06/56] Another cast when multiplying integers. + +--- + rsync.h | 2 +- + sender.c | 2 +- + 2 files changed, 2 insertions(+), 2 deletions(-) + +diff --git a/rsync.h b/rsync.h +index 8ddbe702..0f9e277f 100644 +--- a/rsync.h ++++ b/rsync.h +@@ -982,7 +982,7 @@ struct map_struct { + int status; /* first errno from read errors */ + }; + +-#define sum2_at(s, i) ((s)->sum2_array + ((OFF_T)(i) * xfer_sum_len)) ++#define sum2_at(s, i) ((s)->sum2_array + ((size_t)(i) * xfer_sum_len)) + + #define NAME_IS_FILE (0) /* filter name as a file */ + #define NAME_IS_DIR (1<<0) /* filter name as a dir */ +diff --git a/sender.c b/sender.c +index ab205341..2bbff2fa 100644 +--- a/sender.c ++++ b/sender.c +@@ -95,7 +95,7 @@ static struct sum_struct *receive_sums(int f) + return(s); + + s->sums = new_array(struct sum_buf, s->count); +- s->sum2_array = new_array(char, s->count * xfer_sum_len); ++ s->sum2_array = new_array(char, (size_t)s->count * xfer_sum_len); + + for (i = 0; i < s->count; i++) { + s->sums[i].sum1 = read_int(f); +-- +2.51.0 + diff -Nru rsync-3.2.7/debian/patches/2026-05-20/0007-prevent-information-leak-off-the-stack.patch rsync-3.2.7/debian/patches/2026-05-20/0007-prevent-information-leak-off-the-stack.patch --- rsync-3.2.7/debian/patches/2026-05-20/0007-prevent-information-leak-off-the-stack.patch 1970-01-01 00:00:00.000000000 +0000 +++ rsync-3.2.7/debian/patches/2026-05-20/0007-prevent-information-leak-off-the-stack.patch 2026-05-20 05:47:50.000000000 +0000 @@ -0,0 +1,27 @@ +From ce06002ce8cee8fdfaed80f366aa454d9c0613f4 Mon Sep 17 00:00:00 2001 +From: Andrew Tridgell +Date: Thu, 14 Nov 2024 09:57:08 +1100 +Subject: [PATCH 07/56] prevent information leak off the stack + +prevent leak of uninitialised stack data in hash_search +--- + match.c | 3 +++ + 1 file changed, 3 insertions(+) + +diff --git a/match.c b/match.c +index 94d77b87..10e60bd3 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; +-- +2.51.0 + diff -Nru rsync-3.2.7/debian/patches/2026-05-20/0008-refuse-fuzzy-options-when-fuzzy-not-selected.patch rsync-3.2.7/debian/patches/2026-05-20/0008-refuse-fuzzy-options-when-fuzzy-not-selected.patch --- rsync-3.2.7/debian/patches/2026-05-20/0008-refuse-fuzzy-options-when-fuzzy-not-selected.patch 1970-01-01 00:00:00.000000000 +0000 +++ rsync-3.2.7/debian/patches/2026-05-20/0008-refuse-fuzzy-options-when-fuzzy-not-selected.patch 2026-05-20 05:47:50.000000000 +0000 @@ -0,0 +1,37 @@ +From 769e5f0bcf58c360b635b5720b408e362a8d72b4 Mon Sep 17 00:00:00 2001 +From: Andrew Tridgell +Date: Sat, 23 Nov 2024 11:08:03 +1100 +Subject: [PATCH 08/56] refuse fuzzy options when fuzzy not selected + +this prevents a malicious server providing a file to compare to when +the user has not given the fuzzy option +--- + receiver.c | 5 +++++ + 1 file changed, 5 insertions(+) + +diff --git a/receiver.c b/receiver.c +index c9d7e01d..6a2ae620 100644 +--- a/receiver.c ++++ b/receiver.c +@@ -66,6 +66,7 @@ extern char sender_file_sum[MAX_DIGEST_LEN]; + extern struct file_list *cur_flist, *first_flist, *dir_flist; + extern filter_rule_list daemon_filter_list; + extern OFF_T preallocated_len; ++extern int fuzzy_basis; + + extern struct name_num_item *xfer_sum_nni; + extern int xfer_sum_len; +@@ -716,6 +717,10 @@ int recv_files(int f_in, int f_out, char *local_name) + fnamecmp = get_backup_name(fname); + break; + case FNAMECMP_FUZZY: ++ if (fuzzy_basis == 0) { ++ rprintf(FERROR_XFER, "rsync: refusing malicious fuzzy operation for %s\n", xname); ++ exit_cleanup(RERR_PROTOCOL); ++ } + if (file->dirname) { + pathjoin(fnamecmpbuf, sizeof fnamecmpbuf, file->dirname, xname); + fnamecmp = fnamecmpbuf; +-- +2.51.0 + diff -Nru rsync-3.2.7/debian/patches/2026-05-20/0009-added-secure_relative_open.patch rsync-3.2.7/debian/patches/2026-05-20/0009-added-secure_relative_open.patch --- rsync-3.2.7/debian/patches/2026-05-20/0009-added-secure_relative_open.patch 1970-01-01 00:00:00.000000000 +0000 +++ rsync-3.2.7/debian/patches/2026-05-20/0009-added-secure_relative_open.patch 2026-05-20 05:47:50.000000000 +0000 @@ -0,0 +1,103 @@ +From b31301abb7526328730bcd6468fb657c3e6eace9 Mon Sep 17 00:00:00 2001 +From: Andrew Tridgell +Date: Sat, 23 Nov 2024 12:26:10 +1100 +Subject: [PATCH 09/56] added secure_relative_open() + +this is an open that enforces no symlink following for all path +components in a relative path +--- + syscall.c | 74 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ + 1 file changed, 74 insertions(+) + +diff --git a/syscall.c b/syscall.c +index b4b0f1f1..cffc814b 100644 +--- a/syscall.c ++++ b/syscall.c +@@ -33,6 +33,8 @@ + #include + #endif + ++#include "ifuncs.h" ++ + extern int dry_run; + extern int am_root; + extern int am_sender; +@@ -707,3 +709,75 @@ int do_open_nofollow(const char *pathname, int flags) + + return fd; + } ++ ++/* ++ open a file relative to a base directory. The basedir can be NULL, ++ in which case the current working directory is used. The relpath ++ must be a relative path, and the relpath must not contain any ++ elements in the path which follow symlinks (ie. like O_NOFOLLOW, but ++ applies to all path components, not just the last component) ++*/ ++int secure_relative_open(const char *basedir, const char *relpath, int flags, mode_t mode) ++{ ++ if (!relpath || relpath[0] == '/') { ++ // must be a relative path ++ errno = EINVAL; ++ return -1; ++ } ++ ++#if !defined(O_NOFOLLOW) || !defined(O_DIRECTORY) ++ // really old system, all we can do is live with the risks ++ if (!basedir) { ++ return open(relpath, flags, mode); ++ } ++ char fullpath[MAXPATHLEN]; ++ pathjoin(fullpath, sizeof fullpath, basedir, relpath); ++ return open(fullpath, flags, mode); ++#else ++ int dirfd = AT_FDCWD; ++ if (basedir != NULL) { ++ dirfd = openat(AT_FDCWD, basedir, O_RDONLY | O_DIRECTORY); ++ if (dirfd == -1) { ++ return -1; ++ } ++ } ++ int retfd = -1; ++ ++ char *path_copy = my_strdup(relpath, __FILE__, __LINE__); ++ if (!path_copy) { ++ return -1; ++ } ++ ++ for (const char *part = strtok(path_copy, "/"); ++ part != NULL; ++ part = strtok(NULL, "/")) ++ { ++ int next_fd = openat(dirfd, part, O_RDONLY | O_DIRECTORY | O_NOFOLLOW); ++ if (next_fd == -1 && errno == ENOTDIR) { ++ if (strtok(NULL, "/") != NULL) { ++ // this is not the last component of the path ++ errno = ELOOP; ++ goto cleanup; ++ } ++ // this could be the last component of the path, try as a file ++ retfd = openat(dirfd, part, flags | O_NOFOLLOW, mode); ++ goto cleanup; ++ } ++ if (next_fd == -1) { ++ goto cleanup; ++ } ++ if (dirfd != AT_FDCWD) close(dirfd); ++ dirfd = next_fd; ++ } ++ ++ // the path must be a directory ++ errno = EINVAL; ++ ++cleanup: ++ free(path_copy); ++ if (dirfd != AT_FDCWD) { ++ close(dirfd); ++ } ++ return retfd; ++#endif // O_NOFOLLOW, O_DIRECTORY ++} +-- +2.51.0 + diff -Nru rsync-3.2.7/debian/patches/2026-05-20/0010-receiver-use-secure_relative_open-for-basis-file.patch rsync-3.2.7/debian/patches/2026-05-20/0010-receiver-use-secure_relative_open-for-basis-file.patch --- rsync-3.2.7/debian/patches/2026-05-20/0010-receiver-use-secure_relative_open-for-basis-file.patch 1970-01-01 00:00:00.000000000 +0000 +++ rsync-3.2.7/debian/patches/2026-05-20/0010-receiver-use-secure_relative_open-for-basis-file.patch 2026-05-20 05:47:50.000000000 +0000 @@ -0,0 +1,103 @@ +From 866dd7131e7c7ed3fe8fea932ab88816e68f53f2 Mon Sep 17 00:00:00 2001 +From: Andrew Tridgell +Date: Sat, 23 Nov 2024 12:28:13 +1100 +Subject: [PATCH 10/56] receiver: use secure_relative_open() for basis file + +this prevents attacks where the basis file is manipulated by a +malicious sender to gain information about files outside the +destination tree +--- + receiver.c | 42 ++++++++++++++++++++++++++---------------- + 1 file changed, 26 insertions(+), 16 deletions(-) + +diff --git a/receiver.c b/receiver.c +index 6a2ae620..a92f1049 100644 +--- a/receiver.c ++++ b/receiver.c +@@ -552,6 +552,8 @@ int recv_files(int f_in, int f_out, char *local_name) + progress_init(); + + while (1) { ++ const char *basedir = NULL; ++ + cleanup_disable(); + + /* This call also sets cur_flist. */ +@@ -722,27 +724,29 @@ int recv_files(int f_in, int f_out, char *local_name) + exit_cleanup(RERR_PROTOCOL); + } + if (file->dirname) { +- pathjoin(fnamecmpbuf, sizeof fnamecmpbuf, file->dirname, xname); +- fnamecmp = fnamecmpbuf; +- } else +- fnamecmp = xname; ++ basedir = file->dirname; ++ } ++ fnamecmp = xname; + break; + default: + if (fnamecmp_type > FNAMECMP_FUZZY && fnamecmp_type-FNAMECMP_FUZZY <= basis_dir_cnt) { + fnamecmp_type -= FNAMECMP_FUZZY + 1; + if (file->dirname) { +- stringjoin(fnamecmpbuf, sizeof fnamecmpbuf, +- basis_dir[fnamecmp_type], "/", file->dirname, "/", xname, NULL); +- } else +- pathjoin(fnamecmpbuf, sizeof fnamecmpbuf, basis_dir[fnamecmp_type], xname); ++ pathjoin(fnamecmpbuf, sizeof fnamecmpbuf, basis_dir[fnamecmp_type], file->dirname); ++ basedir = fnamecmpbuf; ++ } else { ++ basedir = basis_dir[fnamecmp_type]; ++ } ++ fnamecmp = xname; + } else if (fnamecmp_type >= basis_dir_cnt) { + rprintf(FERROR, + "invalid basis_dir index: %d.\n", + fnamecmp_type); + exit_cleanup(RERR_PROTOCOL); +- } else +- pathjoin(fnamecmpbuf, sizeof fnamecmpbuf, basis_dir[fnamecmp_type], fname); +- fnamecmp = fnamecmpbuf; ++ } else { ++ basedir = basis_dir[fnamecmp_type]; ++ fnamecmp = fname; ++ } + break; + } + if (!fnamecmp || (daemon_filter_list.head +@@ -765,7 +769,7 @@ int recv_files(int f_in, int f_out, char *local_name) + } + + /* open the file */ +- fd1 = do_open(fnamecmp, O_RDONLY, 0); ++ fd1 = secure_relative_open(basedir, fnamecmp, O_RDONLY, 0); + + if (fd1 == -1 && protocol_version < 29) { + if (fnamecmp != fname) { +@@ -776,14 +780,20 @@ int recv_files(int f_in, int f_out, char *local_name) + + if (fd1 == -1 && basis_dir[0]) { + /* pre-29 allowed only one alternate basis */ +- pathjoin(fnamecmpbuf, sizeof fnamecmpbuf, +- basis_dir[0], fname); +- fnamecmp = fnamecmpbuf; ++ basedir = basis_dir[0]; ++ fnamecmp = fname; + fnamecmp_type = FNAMECMP_BASIS_DIR_LOW; +- fd1 = do_open(fnamecmp, O_RDONLY, 0); ++ fd1 = secure_relative_open(basedir, fnamecmp, O_RDONLY, 0); + } + } + ++ if (basedir) { ++ // for the following code we need the full ++ // path name as a single string ++ pathjoin(fnamecmpbuf, sizeof fnamecmpbuf, basedir, fnamecmp); ++ fnamecmp = fnamecmpbuf; ++ } ++ + one_inplace = inplace_partial && fnamecmp_type == FNAMECMP_PARTIAL_DIR; + updating_basis_or_equiv = one_inplace + || (inplace && (fnamecmp == fname || fnamecmp_type == FNAMECMP_BACKUP)); +-- +2.51.0 + diff -Nru rsync-3.2.7/debian/patches/2026-05-20/0011-disallow-.-elements-in-relpath-for-secure_relative_o.patch rsync-3.2.7/debian/patches/2026-05-20/0011-disallow-.-elements-in-relpath-for-secure_relative_o.patch --- rsync-3.2.7/debian/patches/2026-05-20/0011-disallow-.-elements-in-relpath-for-secure_relative_o.patch 1970-01-01 00:00:00.000000000 +0000 +++ rsync-3.2.7/debian/patches/2026-05-20/0011-disallow-.-elements-in-relpath-for-secure_relative_o.patch 2026-05-20 05:47:50.000000000 +0000 @@ -0,0 +1,38 @@ +From 16666c6e75953edc377175594359e2142419769d Mon Sep 17 00:00:00 2001 +From: Andrew Tridgell +Date: Tue, 26 Nov 2024 09:16:31 +1100 +Subject: [PATCH 11/56] disallow ../ elements in relpath for + secure_relative_open + +--- + syscall.c | 7 +++++++ + 1 file changed, 7 insertions(+) + +diff --git a/syscall.c b/syscall.c +index cffc814b..081357bb 100644 +--- a/syscall.c ++++ b/syscall.c +@@ -716,6 +716,8 @@ int do_open_nofollow(const char *pathname, int flags) + must be a relative path, and the relpath must not contain any + elements in the path which follow symlinks (ie. like O_NOFOLLOW, but + applies to all path components, not just the last component) ++ ++ The relpath must also not contain any ../ elements in the path + */ + int secure_relative_open(const char *basedir, const char *relpath, int flags, mode_t mode) + { +@@ -724,6 +726,11 @@ int secure_relative_open(const char *basedir, const char *relpath, int flags, mo + errno = EINVAL; + return -1; + } ++ if (strncmp(relpath, "../", 3) == 0 || strstr(relpath, "/../")) { ++ // no ../ elements allowed in the relpath ++ errno = EINVAL; ++ return -1; ++ } + + #if !defined(O_NOFOLLOW) || !defined(O_DIRECTORY) + // really old system, all we can do is live with the risks +-- +2.51.0 + diff -Nru rsync-3.2.7/debian/patches/2026-05-20/0012-Refuse-a-duplicate-dirlist.patch rsync-3.2.7/debian/patches/2026-05-20/0012-Refuse-a-duplicate-dirlist.patch --- rsync-3.2.7/debian/patches/2026-05-20/0012-Refuse-a-duplicate-dirlist.patch 1970-01-01 00:00:00.000000000 +0000 +++ rsync-3.2.7/debian/patches/2026-05-20/0012-Refuse-a-duplicate-dirlist.patch 2026-05-20 05:47:50.000000000 +0000 @@ -0,0 +1,45 @@ +From 14026162dcf0b07ac977b3df1c060278bc0c0bf6 Mon Sep 17 00:00:00 2001 +From: Wayne Davison +Date: Thu, 14 Nov 2024 15:46:50 -0800 +Subject: [PATCH 12/56] Refuse a duplicate dirlist. + +--- + flist.c | 9 +++++++++ + rsync.h | 1 + + 2 files changed, 10 insertions(+) + +diff --git a/flist.c b/flist.c +index 65b459b1..b8f66987 100644 +--- a/flist.c ++++ b/flist.c +@@ -2584,6 +2584,15 @@ struct file_list *recv_file_list(int f, int dir_ndx) + init_hard_links(); + #endif + ++ if (inc_recurse && dir_ndx >= 0) { ++ 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"); + flist_expand(flist, FLIST_START_LARGE); + +diff --git a/rsync.h b/rsync.h +index 0f9e277f..b9a7101a 100644 +--- a/rsync.h ++++ b/rsync.h +@@ -84,6 +84,7 @@ + #define FLAG_DUPLICATE (1<<4) /* sender */ + #define FLAG_MISSING_DIR (1<<4) /* generator */ + #define FLAG_HLINKED (1<<5) /* receiver/generator (checked on all types) */ ++#define FLAG_GOT_DIR_FLIST (1<<5)/* sender/receiver/generator - dir_flist only */ + #define FLAG_HLINK_FIRST (1<<6) /* receiver/generator (w/FLAG_HLINKED) */ + #define FLAG_IMPLIED_DIR (1<<6) /* sender/receiver/generator (dirs only) */ + #define FLAG_HLINK_LAST (1<<7) /* receiver/generator */ +-- +2.51.0 + diff -Nru rsync-3.2.7/debian/patches/2026-05-20/0013-range-check-dir_ndx-before-use.patch rsync-3.2.7/debian/patches/2026-05-20/0013-range-check-dir_ndx-before-use.patch --- rsync-3.2.7/debian/patches/2026-05-20/0013-range-check-dir_ndx-before-use.patch 1970-01-01 00:00:00.000000000 +0000 +++ rsync-3.2.7/debian/patches/2026-05-20/0013-range-check-dir_ndx-before-use.patch 2026-05-20 05:47:50.000000000 +0000 @@ -0,0 +1,27 @@ +From de7a95a77597faf52b7290119f06e2577030a1b5 Mon Sep 17 00:00:00 2001 +From: Andrew Tridgell +Date: Tue, 26 Nov 2024 16:12:45 +1100 +Subject: [PATCH 13/56] range check dir_ndx before use + +--- + flist.c | 4 ++++ + 1 file changed, 4 insertions(+) + +diff --git a/flist.c b/flist.c +index b8f66987..929b59d2 100644 +--- a/flist.c ++++ b/flist.c +@@ -2585,6 +2585,10 @@ struct file_list *recv_file_list(int f, int dir_ndx) + #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); +-- +2.51.0 + diff -Nru rsync-3.2.7/debian/patches/2026-05-20/0014-make-safe-links-stricter.patch rsync-3.2.7/debian/patches/2026-05-20/0014-make-safe-links-stricter.patch --- rsync-3.2.7/debian/patches/2026-05-20/0014-make-safe-links-stricter.patch 1970-01-01 00:00:00.000000000 +0000 +++ rsync-3.2.7/debian/patches/2026-05-20/0014-make-safe-links-stricter.patch 2026-05-20 05:47:50.000000000 +0000 @@ -0,0 +1,136 @@ +From 5ddf47027525e6ad5471a0785af3d3eeeeb9dd84 Mon Sep 17 00:00:00 2001 +From: Andrew Tridgell +Date: Sat, 23 Nov 2024 15:15:53 +1100 +Subject: [PATCH 14/56] make --safe-links stricter + +when --safe-links is used also reject links where a '../' component is +included in the destination as other than the leading part of the +filename +--- + testsuite/safe-links.test | 55 ++++++++++++++++++++++++++++++++++++ + testsuite/unsafe-byname.test | 2 +- + util1.c | 26 ++++++++++++++++- + 3 files changed, 81 insertions(+), 2 deletions(-) + create mode 100644 testsuite/safe-links.test + +diff --git a/testsuite/safe-links.test b/testsuite/safe-links.test +new file mode 100644 +index 00000000..6e95a4b9 +--- /dev/null ++++ b/testsuite/safe-links.test +@@ -0,0 +1,55 @@ ++#!/bin/sh ++ ++. "$suitedir/rsync.fns" ++ ++test_symlink() { ++ is_a_link "$1" || test_fail "File $1 is not a symlink" ++} ++ ++test_regular() { ++ if [ ! -f "$1" ]; then ++ test_fail "File $1 is not regular file or not exists" ++ fi ++} ++ ++test_notexist() { ++ if [ -e "$1" ]; then ++ test_fail "File $1 exists" ++ fi ++ if [ -h "$1" ]; then ++ test_fail "File $1 exists as a symlink" ++ fi ++} ++ ++cd "$tmpdir" ++ ++mkdir from ++ ++mkdir "from/safe" ++mkdir "from/unsafe" ++ ++mkdir "from/safe/files" ++mkdir "from/safe/links" ++ ++touch "from/safe/files/file1" ++touch "from/safe/files/file2" ++touch "from/unsafe/unsafefile" ++ ++ln -s ../files/file1 "from/safe/links/" ++ln -s ../files/file2 "from/safe/links/" ++ln -s ../../unsafe/unsafefile "from/safe/links/" ++ln -s a/a/a/../../../unsafe2 "from/safe/links/" ++ ++#echo "LISTING FROM" ++#ls -lR from ++ ++echo "rsync with relative path and just -a" ++$RSYNC -avv --safe-links from/safe/ to ++ ++#echo "LISTING TO" ++#ls -lR to ++ ++test_symlink to/links/file1 ++test_symlink to/links/file2 ++test_notexist to/links/unsafefile ++test_notexist to/links/unsafe2 +diff --git a/testsuite/unsafe-byname.test b/testsuite/unsafe-byname.test +index 75e72014..d2e318ef 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/util1.c b/util1.c +index da50ff1e..f260d398 100644 +--- a/util1.c ++++ b/util1.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. */ +-- +2.51.0 + diff -Nru rsync-3.2.7/debian/patches/2026-05-20/0015-fixed-symlink-race-condition-in-sender.patch rsync-3.2.7/debian/patches/2026-05-20/0015-fixed-symlink-race-condition-in-sender.patch --- rsync-3.2.7/debian/patches/2026-05-20/0015-fixed-symlink-race-condition-in-sender.patch 1970-01-01 00:00:00.000000000 +0000 +++ rsync-3.2.7/debian/patches/2026-05-20/0015-fixed-symlink-race-condition-in-sender.patch 2026-05-20 05:47:50.000000000 +0000 @@ -0,0 +1,187 @@ +From ad7dc537954c7e57a75942d74850a15c24b8275e Mon Sep 17 00:00:00 2001 +From: Andrew Tridgell +Date: Wed, 18 Dec 2024 08:59:42 +1100 +Subject: [PATCH 15/56] fixed symlink race condition in sender + +when we open a file that we don't expect to be a symlink use +O_NOFOLLOW to prevent a race condition where an attacker could change +a file between being a normal file and a symlink +--- + checksum.c | 2 +- + flist.c | 2 +- + generator.c | 4 ++-- + receiver.c | 2 +- + sender.c | 2 +- + syscall.c | 20 ++++++++++++++++++++ + t_unsafe.c | 3 +++ + tls.c | 3 +++ + trimslash.c | 2 ++ + util1.c | 2 +- + 10 files changed, 35 insertions(+), 7 deletions(-) + +diff --git a/checksum.c b/checksum.c +index 60de3655..46a224e8 100644 +--- a/checksum.c ++++ b/checksum.c +@@ -405,7 +405,7 @@ void file_checksum(const char *fname, const STRUCT_STAT *st_p, char *sum) + int32 remainder; + int fd; + +- fd = do_open(fname, O_RDONLY, 0); ++ fd = do_open_checklinks(fname); + if (fd == -1) { + memset(sum, 0, file_sum_len); + return; +diff --git a/flist.c b/flist.c +index 929b59d2..3b801e3f 100644 +--- a/flist.c ++++ b/flist.c +@@ -1390,7 +1390,7 @@ struct file_struct *make_file(const char *fname, struct file_list *flist, + + if (copy_devices && am_sender && IS_DEVICE(st.st_mode)) { + if (st.st_size == 0) { +- int fd = do_open(fname, O_RDONLY, 0); ++ int fd = do_open_checklinks(fname); + if (fd >= 0) { + st.st_size = get_device_size(fd, fname); + close(fd); +diff --git a/generator.c b/generator.c +index 21c4a595..a890a43e 100644 +--- a/generator.c ++++ b/generator.c +@@ -1798,7 +1798,7 @@ static void recv_generator(char *fname, struct file_struct *file, int ndx, + + if (write_devices && IS_DEVICE(sx.st.st_mode) && sx.st.st_size == 0) { + /* This early open into fd skips the regular open below. */ +- if ((fd = do_open(fnamecmp, O_RDONLY, 0)) >= 0) ++ if ((fd = do_open_nofollow(fnamecmp, O_RDONLY)) >= 0) + real_sx.st.st_size = sx.st.st_size = get_device_size(fd, fnamecmp); + } + +@@ -1867,7 +1867,7 @@ static void recv_generator(char *fname, struct file_struct *file, int ndx, + } + + /* open the file */ +- if (fd < 0 && (fd = do_open(fnamecmp, O_RDONLY, 0)) < 0) { ++ if (fd < 0 && (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 a92f1049..77de8697 100644 +--- a/receiver.c ++++ b/receiver.c +@@ -775,7 +775,7 @@ int recv_files(int f_in, int f_out, char *local_name) + if (fnamecmp != fname) { + fnamecmp = fname; + fnamecmp_type = 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 2bbff2fa..a4d46c39 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 && protocol_version < 28 ? FERROR : FWARNING; +diff --git a/syscall.c b/syscall.c +index 081357bb..8cea2900 100644 +--- a/syscall.c ++++ b/syscall.c +@@ -45,6 +45,8 @@ extern int preallocate_files; + extern int preserve_perms; + extern int preserve_executability; + extern int open_noatime; ++extern int copy_links; ++extern int copy_unsafe_links; + + #ifndef S_BLKSIZE + # if defined hpux || defined __hpux__ || defined __hpux +@@ -788,3 +790,21 @@ cleanup: + return retfd; + #endif // O_NOFOLLOW, O_DIRECTORY + } ++ ++/* ++ 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 010cac50..e10619a2 100644 +--- a/t_unsafe.c ++++ b/t_unsafe.c +@@ -28,6 +28,9 @@ int am_root = 0; + int am_sender = 1; + int read_only = 0; + int list_only = 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 e6b0708a..858f8f10 100644 +--- a/tls.c ++++ b/tls.c +@@ -49,6 +49,9 @@ int list_only = 0; + int link_times = 0; + int link_owner = 0; + int nsec_times = 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 1ec928ca..f2774cd7 100644 +--- a/trimslash.c ++++ b/trimslash.c +@@ -26,6 +26,8 @@ int am_root = 0; + int am_sender = 1; + int read_only = 1; + int list_only = 0; ++int copy_links = 0; ++int copy_unsafe_links = 0; + + int + main(int argc, char **argv) +diff --git a/util1.c b/util1.c +index f260d398..d84bc414 100644 +--- a/util1.c ++++ b/util1.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; +-- +2.51.0 + diff -Nru rsync-3.2.7/debian/patches/2026-05-20/0016-syscall-fix-a-Y2038-bug-by-replacing-Int32x32To64-wi.patch rsync-3.2.7/debian/patches/2026-05-20/0016-syscall-fix-a-Y2038-bug-by-replacing-Int32x32To64-wi.patch --- rsync-3.2.7/debian/patches/2026-05-20/0016-syscall-fix-a-Y2038-bug-by-replacing-Int32x32To64-wi.patch 1970-01-01 00:00:00.000000000 +0000 +++ rsync-3.2.7/debian/patches/2026-05-20/0016-syscall-fix-a-Y2038-bug-by-replacing-Int32x32To64-wi.patch 2026-05-20 05:47:50.000000000 +0000 @@ -0,0 +1,29 @@ +From 9141bf1d836064d111aedcd2474dba098f6ba441 Mon Sep 17 00:00:00 2001 +From: Silent +Date: Mon, 13 Jan 2025 15:01:06 +0100 +Subject: [PATCH 16/56] syscall: fix a Y2038 bug by replacing Int32x32To64 with + multiplication + +Int32x32To64 macro internally truncates the arguments to int32, +while time_t is 64-bit on most/all modern platforms. +Therefore, usage of this macro creates a Year 2038 bug. +--- + syscall.c | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/syscall.c b/syscall.c +index 8cea2900..21f9382e 100644 +--- a/syscall.c ++++ b/syscall.c +@@ -480,7 +480,7 @@ int do_SetFileTime(const char *path, time_t crtime) + free(pathw); + if (handle == INVALID_HANDLE_VALUE) + return -1; +- int64 temp_time = Int32x32To64(crtime, 10000000) + 116444736000000000LL; ++ int64 temp_time = (crtime * 10000000LL) + 116444736000000000LL; + FILETIME birth_time; + birth_time.dwLowDateTime = (DWORD)temp_time; + birth_time.dwHighDateTime = (DWORD)(temp_time >> 32); +-- +2.51.0 + diff -Nru rsync-3.2.7/debian/patches/2026-05-20/0017-options.c-Fix-segv-if-poptGetContext-returns-NULL.patch rsync-3.2.7/debian/patches/2026-05-20/0017-options.c-Fix-segv-if-poptGetContext-returns-NULL.patch --- rsync-3.2.7/debian/patches/2026-05-20/0017-options.c-Fix-segv-if-poptGetContext-returns-NULL.patch 1970-01-01 00:00:00.000000000 +0000 +++ rsync-3.2.7/debian/patches/2026-05-20/0017-options.c-Fix-segv-if-poptGetContext-returns-NULL.patch 2026-05-20 05:47:50.000000000 +0000 @@ -0,0 +1,35 @@ +From 350d39a05dd2f44921160252a12afc1550bae99c Mon Sep 17 00:00:00 2001 +From: Ronnie Sahlberg +Date: Thu, 30 Jan 2025 13:27:38 +1000 +Subject: [PATCH 17/56] options.c: Fix segv if poptGetContext returns NULL + +If poptGetContext returns NULL, perhaps due to OOM, +a NULL pointer is passed into poptReadDefaultConfig() +which in turns SEGVs when trying to dereference it. + +This was found using https://github.com/sahlberg/malloc-fail-tester.git +$ ./test_malloc_failure.sh rsync -Pav crash crosh + +Signed-off-by: Ronnie Sahlberg +--- + options.c | 4 ++++ + 1 file changed, 4 insertions(+) + +diff --git a/options.c b/options.c +index d38bbe8d..d488b4dc 100644 +--- a/options.c ++++ b/options.c +@@ -1372,6 +1372,10 @@ int parse_arguments(int *argc_p, const char ***argv_p) + if (pc) + poptFreeContext(pc); + pc = poptGetContext(RSYNC_NAME, argc, argv, long_options, 0); ++ if (pc == NULL) { ++ strlcpy(err_buf, "poptGetContext returned NULL\n", sizeof err_buf); ++ return 0; ++ } + if (!am_server) { + poptReadDefaultConfig(pc, 0); + popt_unalias(pc, "--daemon"); +-- +2.51.0 + diff -Nru rsync-3.2.7/debian/patches/2026-05-20/0018-Using-a-correct-time-in-log-file.patch rsync-3.2.7/debian/patches/2026-05-20/0018-Using-a-correct-time-in-log-file.patch --- rsync-3.2.7/debian/patches/2026-05-20/0018-Using-a-correct-time-in-log-file.patch 1970-01-01 00:00:00.000000000 +0000 +++ rsync-3.2.7/debian/patches/2026-05-20/0018-Using-a-correct-time-in-log-file.patch 2026-05-20 05:47:50.000000000 +0000 @@ -0,0 +1,53 @@ +From ae1bc1b9e767db88610b5a3a2d3e7106a46eef79 Mon Sep 17 00:00:00 2001 +From: Michal Ruprich +Date: Fri, 31 Jan 2025 14:35:18 +0100 +Subject: [PATCH 18/56] Using a correct time in log file + +--- + options.c | 2 +- + tls.c | 2 +- + util1.c | 2 +- + 3 files changed, 3 insertions(+), 3 deletions(-) + +diff --git a/options.c b/options.c +index d488b4dc..f01b25ee 100644 +--- a/options.c ++++ b/options.c +@@ -1155,7 +1155,7 @@ static time_t parse_time(const char *arg) + { + const char *cp; + time_t val, now = time(NULL); +- struct tm t, *today = localtime(&now); ++ struct tm t, tmp, *today = localtime_r(&now, &tmp); + int in_date, old_mday, n; + + memset(&t, 0, sizeof t); +diff --git a/tls.c b/tls.c +index 858f8f10..7811e1fc 100644 +--- a/tls.c ++++ b/tls.c +@@ -127,7 +127,7 @@ static void storetime(char *dest, size_t destsize, time_t t, int nsecs) + { + if (t) { + int len; +- struct tm *mt = gmtime(&t); ++ struct tm tmp, *mt = gmtime_r(&t, &tmp); + + len = snprintf(dest, destsize, + " %04d-%02d-%02d %02d:%02d:%02d", +diff --git a/util1.c b/util1.c +index d84bc414..231d2206 100644 +--- a/util1.c ++++ b/util1.c +@@ -1389,7 +1389,7 @@ char *timestring(time_t t) + static int ndx = 0; + static char buffers[4][20]; /* We support 4 simultaneous timestring results. */ + char *TimeBuf = buffers[ndx = (ndx + 1) % 4]; +- struct tm *tm = localtime(&t); ++ struct tm tmp, *tm = localtime_r(&t, &tmp); + int len = snprintf(TimeBuf, sizeof buffers[0], "%4d/%02d/%02d %02d:%02d:%02d", + (int)tm->tm_year + 1900, (int)tm->tm_mon + 1, (int)tm->tm_mday, + (int)tm->tm_hour, (int)tm->tm_min, (int)tm->tm_sec); +-- +2.51.0 + diff -Nru rsync-3.2.7/debian/patches/2026-05-20/0019-configure.ac-check-for-xattr-support-both-in-libc-an.patch rsync-3.2.7/debian/patches/2026-05-20/0019-configure.ac-check-for-xattr-support-both-in-libc-an.patch --- rsync-3.2.7/debian/patches/2026-05-20/0019-configure.ac-check-for-xattr-support-both-in-libc-an.patch 1970-01-01 00:00:00.000000000 +0000 +++ rsync-3.2.7/debian/patches/2026-05-20/0019-configure.ac-check-for-xattr-support-both-in-libc-an.patch 2026-05-20 05:47:50.000000000 +0000 @@ -0,0 +1,54 @@ +From 841ab71c7c1e8ce62d09263e4a4941f158b419d0 Mon Sep 17 00:00:00 2001 +From: Eli Schwartz +Date: Tue, 22 Apr 2025 16:17:55 -0400 +Subject: [PATCH 19/56] configure.ac: check for xattr support both in libc and + in -lattr + +In 2015, the attr/xattr.h header was fully removed from upstream attr. + +In 2020, rsync started preferring the standard header, if it exists: +https://github.com/RsyncProject/rsync/pull/22 + +But the fix was incomplete. We still looked for the getxattr function in +-lattr, and used it if -lattr exists. This was the case even if the +system libc was sufficient to provide the needed functions. Result: +overlinking to -lattr, if it happened to be installed for any other +reason. + +``` +checking whether to support extended attributes... Using Linux xattrs +checking for getxattr in -lattr... yes +``` + +Instead, use a different autoconf macro that first checks if the +function is available for use without any libraries (e.g. it is in +libc). + +Result: + +``` +checking whether to support extended attributes... Using Linux xattrs +checking for library containing getxattr... none required +``` + +Signed-off-by: Eli Schwartz +--- + configure.ac | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/configure.ac b/configure.ac +index a2c99558..87b900c5 100644 +--- a/configure.ac ++++ b/configure.ac +@@ -1390,7 +1390,7 @@ else + AC_DEFINE(HAVE_LINUX_XATTRS, 1, [True if you have Linux xattrs (or equivalent)]) + AC_DEFINE(SUPPORT_XATTRS, 1) + AC_DEFINE(NO_SYMLINK_USER_XATTRS, 1, [True if symlinks do not support user xattrs]) +- AC_CHECK_LIB(attr,getxattr) ++ AC_SEARCH_LIBS(getxattr,attr) + ;; + darwin*) + AC_MSG_RESULT(Using OS X xattrs) +-- +2.51.0 + diff -Nru rsync-3.2.7/debian/patches/2026-05-20/0020-util-fixed-issue-in-clean_fname.patch rsync-3.2.7/debian/patches/2026-05-20/0020-util-fixed-issue-in-clean_fname.patch --- rsync-3.2.7/debian/patches/2026-05-20/0020-util-fixed-issue-in-clean_fname.patch 1970-01-01 00:00:00.000000000 +0000 +++ rsync-3.2.7/debian/patches/2026-05-20/0020-util-fixed-issue-in-clean_fname.patch 2026-05-20 05:47:50.000000000 +0000 @@ -0,0 +1,43 @@ +From 934a6bec964fd499ffb7a5cab60a2c2aa4e70bbc Mon Sep 17 00:00:00 2001 +From: Andrew Tridgell +Date: Sat, 23 Aug 2025 19:14:59 +1000 +Subject: [PATCH 20/56] util: fixed issue in clean_fname() + +fixes buffer underflow (not exploitable) in clean_fname +--- + util1.c | 12 ++++++++---- + 1 file changed, 8 insertions(+), 4 deletions(-) + +diff --git a/util1.c b/util1.c +index 231d2206..de634a84 100644 +--- a/util1.c ++++ b/util1.c +@@ -942,7 +942,7 @@ int count_dir_elements(const char *p) + * resulting name would be empty, returns ".". */ + int clean_fname(char *name, int flags) + { +- char *limit = name - 1, *t = name, *f = name; ++ char *limit = name, *t = name, *f = name; + int anchored; + + if (!name) +@@ -987,9 +987,13 @@ int clean_fname(char *name, int flags) + f += 2; + continue; + } +- while (s > limit && *--s != '/') {} +- if (s != t - 1 && (s < name || *s == '/')) { +- t = s + 1; ++ /* backing up for ".." — avoid reading before 'name' */ ++ while (s > limit && s[-1] != '/') ++ s--; ++ ++ /* If found prior '/', or we reached the start, adjust t. */ ++ if (s != t - 1 && (s <= name || *s == '/')) { ++ t = (s == name) ? name : s + 1; + f += 2; + continue; + } +-- +2.51.0 + diff -Nru rsync-3.2.7/debian/patches/2026-05-20/0021-testsuite-added-clean-fname-underflow-test.patch rsync-3.2.7/debian/patches/2026-05-20/0021-testsuite-added-clean-fname-underflow-test.patch --- rsync-3.2.7/debian/patches/2026-05-20/0021-testsuite-added-clean-fname-underflow-test.patch 1970-01-01 00:00:00.000000000 +0000 +++ rsync-3.2.7/debian/patches/2026-05-20/0021-testsuite-added-clean-fname-underflow-test.patch 2026-05-20 05:47:50.000000000 +0000 @@ -0,0 +1,85 @@ +From 03d0b41e64cf3e81e1250efb5e2adfd41e4d39d1 Mon Sep 17 00:00:00 2001 +From: Andrew Tridgell +Date: Sat, 23 Aug 2025 18:29:06 +1000 +Subject: [PATCH 21/56] testsuite: added clean-fname-underflow test + +--- + testsuite/clean-fname-underflow.test | 66 ++++++++++++++++++++++++++++ + 1 file changed, 66 insertions(+) + create mode 100644 testsuite/clean-fname-underflow.test + +diff --git a/testsuite/clean-fname-underflow.test b/testsuite/clean-fname-underflow.test +new file mode 100644 +index 00000000..56d4fece +--- /dev/null ++++ b/testsuite/clean-fname-underflow.test +@@ -0,0 +1,66 @@ ++#!/bin/sh ++# clean-fname-underflow.test ++# Ensure clean_fname() does not read-before-buffer when collapsing "..". ++# This exercises the --server path where a crafted merge filename hits clean_fname(). ++# ++# Usage: ++# ./configure && make ++# make check TESTS='clean-fname-underflow.test' ++ ++set -eu ++ ++# Try to find the just-built rsync binary if RSYNC_BIN isn't set. ++if [ -z "${RSYNC_BIN:-}" ]; then ++ if [ -x "./rsync" ]; then ++ RSYNC_BIN=./rsync ++ elif [ -x "../rsync" ]; then ++ RSYNC_BIN=../rsync ++ else ++ RSYNC_BIN=rsync ++ fi ++fi ++ ++workdir="${TMPDIR:-/tmp}/rsync-clean-fname.$$" ++mkdir -p "$workdir" ++trap 'rm -rf "$workdir"' EXIT INT TERM ++cd "$workdir" ++ ++# Minimal rsyncd.conf using chroot so the crafted path reaches the server parser. ++cat > rsyncd.conf <<'EOF' ++pid file = rsyncd.pid ++use chroot = true ++[mod] ++ path = ./mod ++ read only = false ++EOF ++mkdir -p mod ++ ++# Start daemon on a random high port. ++PORT=$(awk 'BEGIN{srand(); printf "%d", 20000+int(rand()*20000)}') ++"$RSYNC_BIN" --daemon --no-detach --config=rsyncd.conf --port="$PORT" >/dev/null 2>&1 & ++DAEMON_PID=$! ++# Give the daemon a moment to come up. ++sleep 0.3 ++ ++# Invoke the server-side path. We don't need a real transfer; we just want to ++# ensure clean_fname() doesn't crash when given "a/../test" via --filter=merge. ++EXIT_OK=0 ++if "$RSYNC_BIN" --server --sender -vlr --filter='merge a/../test' . mod/ >/dev/null 2>&1; then ++ EXIT_OK=1 ++else ++ status=$? ++ # Non-zero exit is expected for bogus input; ensure it wasn't a signal/crash. ++ if [ $status -lt 128 ]; then ++ EXIT_OK=1 ++ fi ++fi ++ ++kill "$DAEMON_PID" >/dev/null 2>&1 || true ++ ++if [ "$EXIT_OK" -ne 1 ]; then ++ echo "clean-fname-underflow.test: rsync exited due to a signal or unexpected status" ++ exit 1 ++fi ++ ++echo "OK: clean_fname() handled 'a/../test' without crashing" ++exit 0 +-- +2.51.0 + diff -Nru rsync-3.2.7/debian/patches/2026-05-20/0022-fixed-an-invalid-access-to-files-array.patch rsync-3.2.7/debian/patches/2026-05-20/0022-fixed-an-invalid-access-to-files-array.patch --- rsync-3.2.7/debian/patches/2026-05-20/0022-fixed-an-invalid-access-to-files-array.patch 1970-01-01 00:00:00.000000000 +0000 +++ rsync-3.2.7/debian/patches/2026-05-20/0022-fixed-an-invalid-access-to-files-array.patch 2026-05-20 05:47:50.000000000 +0000 @@ -0,0 +1,30 @@ +From 41213d20d16005f5ed6f9b8bc70933b087d1ba93 Mon Sep 17 00:00:00 2001 +From: Andrew Tridgell +Date: Sat, 23 Aug 2025 17:26:53 +1000 +Subject: [PATCH 22/56] 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 a4d46c39..b1588b70 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)) { +-- +2.51.0 + diff -Nru rsync-3.2.7/debian/patches/2026-05-20/0023-fix-uninitialized-buf1-in-get_checksum2-MD4-path.patch rsync-3.2.7/debian/patches/2026-05-20/0023-fix-uninitialized-buf1-in-get_checksum2-MD4-path.patch --- rsync-3.2.7/debian/patches/2026-05-20/0023-fix-uninitialized-buf1-in-get_checksum2-MD4-path.patch 1970-01-01 00:00:00.000000000 +0000 +++ rsync-3.2.7/debian/patches/2026-05-20/0023-fix-uninitialized-buf1-in-get_checksum2-MD4-path.patch 2026-05-20 05:47:50.000000000 +0000 @@ -0,0 +1,33 @@ +From ada7ba004f556a5200da902f510ecb4eca296d67 Mon Sep 17 00:00:00 2001 +From: Andrew Tridgell +Date: Tue, 30 Dec 2025 16:21:41 +1100 +Subject: [PATCH 23/56] fix uninitialized buf1 in get_checksum2() MD4 path + +The static buf1 pointer was only allocated when len > len1, but on +first call with len == 0, this condition is false (0 > 0), leaving +buf1 NULL when passed to memcpy(). + +Fixes #673 +--- + checksum.c | 5 ++--- + 1 file changed, 2 insertions(+), 3 deletions(-) + +diff --git a/checksum.c b/checksum.c +index 46a224e8..fb501ea9 100644 +--- a/checksum.c ++++ b/checksum.c +@@ -365,9 +365,8 @@ void get_checksum2(char *buf, int32 len, char *sum) + + mdfour_begin(&m); + +- if (len > len1) { +- if (buf1) +- free(buf1); ++ if (len > len1 || !buf1) { ++ free(buf1); + buf1 = new_array(char, len+4); + len1 = len; + } +-- +2.51.0 + diff -Nru rsync-3.2.7/debian/patches/2026-05-20/0024-reject-negative-token-values-in-compressed-stream-re.patch rsync-3.2.7/debian/patches/2026-05-20/0024-reject-negative-token-values-in-compressed-stream-re.patch --- rsync-3.2.7/debian/patches/2026-05-20/0024-reject-negative-token-values-in-compressed-stream-re.patch 1970-01-01 00:00:00.000000000 +0000 +++ rsync-3.2.7/debian/patches/2026-05-20/0024-reject-negative-token-values-in-compressed-stream-re.patch 2026-05-20 05:47:50.000000000 +0000 @@ -0,0 +1,72 @@ +From 50f0add12a4856f6e7d5e6d1b18530fea05d50e6 Mon Sep 17 00:00:00 2001 +From: Andrew Tridgell +Date: Tue, 30 Dec 2025 18:49:34 +1100 +Subject: [PATCH 24/56] reject negative token values in compressed stream + receivers + +Validate that token numbers read from compressed streams are +non-negative. A negative token value would cause the return value +of recv_*_token() to become positive, which callers interpret as +literal data length, but no data pointer is set on this code path. + +While this only causes the receiver to crash (which is process-isolated +and only affects the attacker's own connection), it's still undefined +behavior. + +Reported-by: Will Sergeant +--- + token.c | 21 ++++++++++++++++++--- + 1 file changed, 18 insertions(+), 3 deletions(-) + +diff --git a/token.c b/token.c +index c108b3af..b7a02ea1 100644 +--- a/token.c ++++ b/token.c +@@ -589,8 +589,13 @@ static int32 recv_deflated_token(int f, char **data) + if (flag & TOKEN_REL) { + rx_token += flag & 0x3f; + flag >>= 6; +- } else ++ } else { + rx_token = read_int(f); ++ if (rx_token < 0) { ++ rprintf(FERROR, "invalid token number in compressed stream\n"); ++ exit_cleanup(RERR_PROTOCOL); ++ } ++ } + if (flag & 1) { + rx_run = read_byte(f); + rx_run += read_byte(f) << 8; +@@ -831,8 +836,13 @@ static int32 recv_zstd_token(int f, char **data) + if (flag & TOKEN_REL) { + rx_token += flag & 0x3f; + flag >>= 6; +- } else ++ } else { + rx_token = read_int(f); ++ if (rx_token < 0) { ++ rprintf(FERROR, "invalid token number in compressed stream\n"); ++ exit_cleanup(RERR_PROTOCOL); ++ } ++ } + if (flag & 1) { + rx_run = read_byte(f); + rx_run += read_byte(f) << 8; +@@ -995,8 +1005,13 @@ static int32 recv_compressed_token(int f, char **data) + if (flag & TOKEN_REL) { + rx_token += flag & 0x3f; + flag >>= 6; +- } else ++ } else { + rx_token = read_int(f); ++ if (rx_token < 0) { ++ rprintf(FERROR, "invalid token number in compressed stream\n"); ++ exit_cleanup(RERR_PROTOCOL); ++ } ++ } + if (flag & 1) { + rx_run = read_byte(f); + rx_run += read_byte(f) << 8; +-- +2.51.0 + diff -Nru rsync-3.2.7/debian/patches/2026-05-20/0025-acl-fixed-ACL-ID-mapping-for-non-root.patch rsync-3.2.7/debian/patches/2026-05-20/0025-acl-fixed-ACL-ID-mapping-for-non-root.patch --- rsync-3.2.7/debian/patches/2026-05-20/0025-acl-fixed-ACL-ID-mapping-for-non-root.patch 1970-01-01 00:00:00.000000000 +0000 +++ rsync-3.2.7/debian/patches/2026-05-20/0025-acl-fixed-ACL-ID-mapping-for-non-root.patch 2026-05-20 05:47:50.000000000 +0000 @@ -0,0 +1,26 @@ +From 43e2af4dd3061b28b8dc62f248a5cd56948949ff Mon Sep 17 00:00:00 2001 +From: Andrew Tridgell +Date: Mon, 19 Jan 2026 11:14:40 +1100 +Subject: [PATCH 25/56] acl: fixed ACL ID mapping for non-root + +closes issue #618 +--- + acls.c | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/acls.c b/acls.c +index 3cf12eeb..cd6aadcd 100644 +--- a/acls.c ++++ b/acls.c +@@ -713,7 +713,7 @@ static uchar recv_ida_entries(int f, ida_entries *ent) + else + id = recv_group_name(f, id, NULL); + } else if (access & NAME_IS_USER) { +- if (inc_recurse && am_root && !numeric_ids) ++ if (inc_recurse && !numeric_ids) + id = match_uid(id); + } else { + if (inc_recurse && (!am_root || !numeric_ids)) +-- +2.51.0 + diff -Nru rsync-3.2.7/debian/patches/2026-05-20/0026-fix-uninitialized-mul_one-in-AVX2-checksum-and-add-S.patch rsync-3.2.7/debian/patches/2026-05-20/0026-fix-uninitialized-mul_one-in-AVX2-checksum-and-add-S.patch --- rsync-3.2.7/debian/patches/2026-05-20/0026-fix-uninitialized-mul_one-in-AVX2-checksum-and-add-S.patch 1970-01-01 00:00:00.000000000 +0000 +++ rsync-3.2.7/debian/patches/2026-05-20/0026-fix-uninitialized-mul_one-in-AVX2-checksum-and-add-S.patch 2026-05-20 05:47:50.000000000 +0000 @@ -0,0 +1,205 @@ +From f8d8bba7934f7023cde754b38e6154590cb9133f Mon Sep 17 00:00:00 2001 +From: Andrew Tridgell +Date: Sun, 1 Mar 2026 08:42:04 +1100 +Subject: [PATCH 26/56] fix uninitialized mul_one in AVX2 checksum and add SIMD + checksum test + +The AVX2 get_checksum1_avx2_64() read mul_one before initializing it, +which is undefined behavior. Replace the cmpeq/abs trick with +_mm256_set1_epi8(1) to match the SSSE3 and SSE2 versions. + +Add a TEST_SIMD_CHECKSUM1 test mode that verifies all SIMD paths +(SSE2, SSSE3, AVX2, and the full dispatch chain) produce identical +results to the C reference, across multiple buffer sizes with both +aligned and unaligned buffers. + +Co-Authored-By: Claude Opus 4.6 +--- + Makefile.in | 11 +++- + simd-checksum-x86_64.cpp | 115 ++++++++++++++++++++++++++++++++++- + testsuite/simd-checksum.test | 11 ++++ + 3 files changed, 134 insertions(+), 3 deletions(-) + create mode 100755 testsuite/simd-checksum.test + +diff --git a/Makefile.in b/Makefile.in +index a1253e5d..be3e0fc5 100644 +--- a/Makefile.in ++++ b/Makefile.in +@@ -57,7 +57,8 @@ TLS_OBJ = tls.o syscall.o util2.o t_stub.o lib/compat.o lib/snprintf.o lib/perms + + # Programs we must have to run the test cases + CHECK_PROGS = rsync$(EXEEXT) tls$(EXEEXT) getgroups$(EXEEXT) getfsdev$(EXEEXT) \ +- testrun$(EXEEXT) trimslash$(EXEEXT) t_unsafe$(EXEEXT) wildtest$(EXEEXT) ++ testrun$(EXEEXT) trimslash$(EXEEXT) t_unsafe$(EXEEXT) wildtest$(EXEEXT) \ ++ simdtest$(EXEEXT) + + CHECK_SYMLINKS = testsuite/chown-fake.test testsuite/devices-fake.test testsuite/xattrs-hlink.test + +@@ -334,6 +335,14 @@ wildtest.o: wildtest.c t_stub.o lib/wildmatch.c rsync.h config.h + wildtest$(EXEEXT): wildtest.o lib/compat.o lib/snprintf.o @BUILD_POPT@ + $(CC) $(CFLAGS) $(LDFLAGS) -o $@ wildtest.o lib/compat.o lib/snprintf.o @BUILD_POPT@ $(LIBS) + ++simdtest$(EXEEXT): simd-checksum-x86_64.cpp $(HEADERS) ++ @if test x"@ROLL_SIMD@" != x; then \ ++ $(CXX) -I. $(CXXFLAGS) $(CPPFLAGS) $(LDFLAGS) -DTEST_SIMD_CHECKSUM1 \ ++ -o $@ $(srcdir)/simd-checksum-x86_64.cpp @ROLL_ASM@ $(LIBS); \ ++ else \ ++ touch $@; \ ++ fi ++ + testsuite/chown-fake.test: + ln -s chown.test $(srcdir)/testsuite/chown-fake.test + +diff --git a/simd-checksum-x86_64.cpp b/simd-checksum-x86_64.cpp +index 33f26e92..81910bba 100644 +--- a/simd-checksum-x86_64.cpp ++++ b/simd-checksum-x86_64.cpp +@@ -347,8 +347,7 @@ __attribute__ ((target("avx2"))) MVSTATIC int32 get_checksum1_avx2_64(schar* buf + __m128i tmp = _mm_load_si128((__m128i*) mul_t1_buf); + __m256i mul_t1 = _mm256_cvtepu8_epi16(tmp); + __m256i mul_const = _mm256_broadcastd_epi32(_mm_cvtsi32_si128(4 | (3 << 8) | (2 << 16) | (1 << 24))); +- __m256i mul_one; +- mul_one = _mm256_abs_epi8(_mm256_cmpeq_epi16(mul_one,mul_one)); // set all vector elements to 1 ++ __m256i mul_one = _mm256_set1_epi8(1); + + for (; i < (len-64); i+=64) { + // Load ... 4*[int8*16] +@@ -548,6 +547,118 @@ int main() { + #pragma clang optimize on + #endif /* BENCHMARK_SIMD_CHECKSUM1 */ + ++#ifdef TEST_SIMD_CHECKSUM1 ++ ++static uint32 checksum_via_default(char *buf, int32 len) ++{ ++ uint32 s1 = 0, s2 = 0; ++ get_checksum1_default_1((schar*)buf, len, 0, &s1, &s2); ++ return (s1 & 0xffff) + (s2 << 16); ++} ++ ++static uint32 checksum_via_sse2(char *buf, int32 len) ++{ ++ int32 i; ++ uint32 s1 = 0, s2 = 0; ++ i = get_checksum1_sse2_32((schar*)buf, len, 0, &s1, &s2); ++ get_checksum1_default_1((schar*)buf, len, i, &s1, &s2); ++ return (s1 & 0xffff) + (s2 << 16); ++} ++ ++static uint32 checksum_via_ssse3(char *buf, int32 len) ++{ ++ int32 i; ++ uint32 s1 = 0, s2 = 0; ++ i = get_checksum1_ssse3_32((schar*)buf, len, 0, &s1, &s2); ++ get_checksum1_default_1((schar*)buf, len, i, &s1, &s2); ++ return (s1 & 0xffff) + (s2 << 16); ++} ++ ++static uint32 checksum_via_avx2(char *buf, int32 len) ++{ ++ int32 i; ++ uint32 s1 = 0, s2 = 0; ++#ifdef USE_ROLL_ASM ++ i = get_checksum1_avx2_asm((schar*)buf, len, 0, &s1, &s2); ++#else ++ i = get_checksum1_avx2_64((schar*)buf, len, 0, &s1, &s2); ++#endif ++ get_checksum1_default_1((schar*)buf, len, i, &s1, &s2); ++ return (s1 & 0xffff) + (s2 << 16); ++} ++ ++int main() ++{ ++ static const int sizes[] = {1, 4, 31, 32, 33, 63, 64, 65, 128, 129, 256, 700, 1024, 4096, 65536}; ++ int num_sizes = sizeof(sizes) / sizeof(sizes[0]); ++ int max_size = sizes[num_sizes - 1]; ++ int failures = 0; ++ ++ /* Allocate with extra bytes for unaligned test */ ++ unsigned char *raw = (unsigned char *)malloc(max_size + 64 + 1); ++ if (!raw) { ++ fprintf(stderr, "malloc failed\n"); ++ return 1; ++ } ++ ++ /* Fill with deterministic data */ ++ for (int i = 0; i < max_size + 64 + 1; i++) ++ raw[i] = (i + (i % 3) + (i % 11)) % 256; ++ ++ /* Test with aligned buffer (64-byte aligned) */ ++ unsigned char *aligned = raw + (64 - ((uintptr_t)raw % 64)); ++ ++ /* Test with unaligned buffer (+1 byte offset) */ ++ unsigned char *unaligned = aligned + 1; ++ ++ struct { const char *name; unsigned char *buf; } buffers[] = { ++ {"aligned", aligned}, ++ {"unaligned", unaligned}, ++ }; ++ ++ for (int b = 0; b < 2; b++) { ++ char *buf = (char *)buffers[b].buf; ++ const char *bname = buffers[b].name; ++ ++ for (int s = 0; s < num_sizes; s++) { ++ int32 len = sizes[s]; ++ uint32 ref = checksum_via_default(buf, len); ++ uint32 cs_sse2 = checksum_via_sse2(buf, len); ++ uint32 cs_ssse3 = checksum_via_ssse3(buf, len); ++ uint32 cs_avx2 = checksum_via_avx2(buf, len); ++ uint32 cs_auto = get_checksum1(buf, len); ++ ++ if (cs_sse2 != ref) { ++ printf("FAIL %-9s size=%5d: SSE2=%08x ref=%08x\n", bname, len, cs_sse2, ref); ++ failures++; ++ } ++ if (cs_ssse3 != ref) { ++ printf("FAIL %-9s size=%5d: SSSE3=%08x ref=%08x\n", bname, len, cs_ssse3, ref); ++ failures++; ++ } ++ if (cs_avx2 != ref) { ++ printf("FAIL %-9s size=%5d: AVX2=%08x ref=%08x\n", bname, len, cs_avx2, ref); ++ failures++; ++ } ++ if (cs_auto != ref) { ++ printf("FAIL %-9s size=%5d: auto=%08x ref=%08x\n", bname, len, cs_auto, ref); ++ failures++; ++ } ++ } ++ } ++ ++ free(raw); ++ ++ if (failures) { ++ printf("%d checksum mismatches!\n", failures); ++ return 1; ++ } ++ printf("All SIMD checksum tests passed.\n"); ++ return 0; ++} ++ ++#endif /* TEST_SIMD_CHECKSUM1 */ ++ + #endif /* } USE_ROLL_SIMD */ + #endif /* } __cplusplus */ + #endif /* } __x86_64__ */ +diff --git a/testsuite/simd-checksum.test b/testsuite/simd-checksum.test +new file mode 100755 +index 00000000..cf7dba2e +--- /dev/null ++++ b/testsuite/simd-checksum.test +@@ -0,0 +1,11 @@ ++#!/bin/sh ++ ++# Test SIMD checksum implementations against the C reference ++ ++. "$suitedir/rsync.fns" ++ ++if ! test -x "$TOOLDIR/simdtest"; then ++ test_skipped "simdtest not built (SIMD not available)" ++fi ++ ++"$TOOLDIR/simdtest" +-- +2.51.0 + diff -Nru rsync-3.2.7/debian/patches/2026-05-20/0027-Fix-glibc-2.43-constness-warnings.patch rsync-3.2.7/debian/patches/2026-05-20/0027-Fix-glibc-2.43-constness-warnings.patch --- rsync-3.2.7/debian/patches/2026-05-20/0027-Fix-glibc-2.43-constness-warnings.patch 1970-01-01 00:00:00.000000000 +0000 +++ rsync-3.2.7/debian/patches/2026-05-20/0027-Fix-glibc-2.43-constness-warnings.patch 2026-05-20 05:47:50.000000000 +0000 @@ -0,0 +1,115 @@ +From c822d0c40418d2a14d7c7761e428a9f4085a3bc0 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Holger=20Hoffst=C3=A4tte?= +Date: Mon, 6 Apr 2026 00:44:02 +0200 +Subject: [PATCH 27/56] Fix glibc-2.43 constness warnings +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +Glibc 2.43 added C23 const-preserving overloads to various string functions, +which change the return type depending on the constness of the argument(s). +Currently this leads to warnings from calls to strtok() or strchr(). +Fix this by properly declaring the respective variable types. + +Signed-off-by: Holger Hoffstätte +--- + access.c | 2 +- + checksum.c | 2 +- + compat.c | 4 ++-- + exclude.c | 2 +- + io.c | 4 ++-- + loadparm.c | 2 +- + 6 files changed, 8 insertions(+), 8 deletions(-) + +diff --git a/access.c b/access.c +index b6afce37..b924e0a3 100644 +--- a/access.c ++++ b/access.c +@@ -99,7 +99,7 @@ static void make_mask(char *mask, int plen, int addrlen) + return; + } + +-static int match_address(const char *addr, const char *tok) ++static int match_address(const char *addr, char *tok) + { + char *p; + struct addrinfo hints, *resa, *rest; +diff --git a/checksum.c b/checksum.c +index fb501ea9..b7ef0485 100644 +--- a/checksum.c ++++ b/checksum.c +@@ -176,7 +176,7 @@ void parse_checksum_choice(int final_call) + if (valid_checksums.negotiated_nni) + xfer_sum_nni = file_sum_nni = valid_checksums.negotiated_nni; + else { +- char *cp = checksum_choice ? strchr(checksum_choice, ',') : NULL; ++ const char *cp = checksum_choice ? strchr(checksum_choice, ',') : NULL; + if (cp) { + xfer_sum_nni = parse_csum_name(checksum_choice, cp - checksum_choice); + file_sum_nni = parse_csum_name(cp+1, -1); +diff --git a/compat.c b/compat.c +index a8a6afe8..f80e3214 100644 +--- a/compat.c ++++ b/compat.c +@@ -131,7 +131,7 @@ static const char *client_info; + * of that protocol for it to be advertised as available. */ + static void check_sub_protocol(void) + { +- char *dot; ++ const char *dot; + int their_protocol, their_sub; + int our_sub = get_subprotocol_version(); + +@@ -414,7 +414,7 @@ static const char *getenv_nstr(int ntype) + env_str = ntype == NSTR_COMPRESS ? "zlib" : protocol_version >= 30 ? "md5" : "md4"; + + if (am_server && env_str) { +- char *cp = strchr(env_str, '&'); ++ const char *cp = strchr(env_str, '&'); + if (cp) + env_str = cp + 1; + } +diff --git a/exclude.c b/exclude.c +index ffe55b16..684253e5 100644 +--- a/exclude.c ++++ b/exclude.c +@@ -903,7 +903,7 @@ static int rule_matches(const char *fname, filter_rule *ex, int name_flags) + { + int slash_handling, str_cnt = 0, anchored_match = 0; + int ret_match = ex->rflags & FILTRULE_NEGATE ? 0 : 1; +- char *p, *pattern = ex->pattern; ++ const char *p, *pattern = ex->pattern; + const char *strings[16]; /* more than enough */ + const char *name = fname + (*fname == '/'); + +diff --git a/io.c b/io.c +index bb60eeca..8d1cf7f2 100644 +--- a/io.c ++++ b/io.c +@@ -1158,8 +1158,8 @@ void set_io_timeout(int secs) + + static void check_for_d_option_error(const char *msg) + { +- static char rsync263_opts[] = "BCDHIKLPRSTWabceghlnopqrtuvxz"; +- char *colon; ++ static const char rsync263_opts[] = "BCDHIKLPRSTWabceghlnopqrtuvxz"; ++ const char *colon; + int saw_d = 0; + + if (*msg != 'r' +diff --git a/loadparm.c b/loadparm.c +index 3906bc0f..4f371d77 100644 +--- a/loadparm.c ++++ b/loadparm.c +@@ -178,7 +178,7 @@ static char *expand_vars(const char *str) + + for (t = buf, f = str; bufsize && *f; ) { + if (*f == '%' && isUpper(f+1)) { +- char *percent = strchr(f+1, '%'); ++ const char *percent = strchr(f+1, '%'); + if (percent && percent - f < bufsize) { + char *val; + strlcpy(t, f+1, percent - f); +-- +2.51.0 + diff -Nru rsync-3.2.7/debian/patches/2026-05-20/0028-zlib-convert-K-R-function-definitions-to-ANSI-style.patch rsync-3.2.7/debian/patches/2026-05-20/0028-zlib-convert-K-R-function-definitions-to-ANSI-style.patch --- rsync-3.2.7/debian/patches/2026-05-20/0028-zlib-convert-K-R-function-definitions-to-ANSI-style.patch 1970-01-01 00:00:00.000000000 +0000 +++ rsync-3.2.7/debian/patches/2026-05-20/0028-zlib-convert-K-R-function-definitions-to-ANSI-style.patch 2026-05-20 05:47:50.000000000 +0000 @@ -0,0 +1,1168 @@ +From 7ab20b293f94597f45f92923edc8bb250a27220e Mon Sep 17 00:00:00 2001 +From: Andrew Tridgell +Date: Thu, 16 Apr 2026 13:40:59 +1000 +Subject: [PATCH 28/56] zlib: convert K&R function definitions to ANSI style + +The bundled zlib 1.2.8 used K&R-style function definitions which are +rejected by clang 16+ as hard errors. Convert all 90 functions across +9 files to ANSI-style prototypes. + +Co-Authored-By: Claude Opus 4.6 (1M context) +--- + zlib/adler32.c | 20 ++------ + zlib/compress.c | 16 ++----- + zlib/crc32.c | 46 +++++------------- + zlib/deflate.c | 124 ++++++++++++------------------------------------ + zlib/inffast.c | 4 +- + zlib/inflate.c | 77 ++++++++---------------------- + zlib/inftrees.c | 10 ++-- + zlib/trees.c | 97 ++++++++++--------------------------- + zlib/zutil.c | 33 ++++--------- + 9 files changed, 105 insertions(+), 322 deletions(-) + +diff --git a/zlib/adler32.c b/zlib/adler32.c +index a868f073..eeadf98d 100644 +--- a/zlib/adler32.c ++++ b/zlib/adler32.c +@@ -62,10 +62,7 @@ local uLong adler32_combine_ OF((uLong adler1, uLong adler2, z_off64_t len2)); + #endif + + /* ========================================================================= */ +-uLong ZEXPORT adler32(adler, buf, len) +- uLong adler; +- const Bytef *buf; +- uInt len; ++uLong ZEXPORT adler32(uLong adler, const Bytef *buf, uInt len) + { + unsigned long sum2; + unsigned n; +@@ -133,10 +130,7 @@ uLong ZEXPORT adler32(adler, buf, len) + } + + /* ========================================================================= */ +-local uLong adler32_combine_(adler1, adler2, len2) +- uLong adler1; +- uLong adler2; +- z_off64_t len2; ++local uLong adler32_combine_(uLong adler1, uLong adler2, z_off64_t len2) + { + unsigned long sum1; + unsigned long sum2; +@@ -162,18 +156,12 @@ local uLong adler32_combine_(adler1, adler2, len2) + } + + /* ========================================================================= */ +-uLong ZEXPORT adler32_combine(adler1, adler2, len2) +- uLong adler1; +- uLong adler2; +- z_off_t len2; ++uLong ZEXPORT adler32_combine(uLong adler1, uLong adler2, z_off_t len2) + { + return adler32_combine_(adler1, adler2, len2); + } + +-uLong ZEXPORT adler32_combine64(adler1, adler2, len2) +- uLong adler1; +- uLong adler2; +- z_off64_t len2; ++uLong ZEXPORT adler32_combine64(uLong adler1, uLong adler2, z_off64_t len2) + { + return adler32_combine_(adler1, adler2, len2); + } +diff --git a/zlib/compress.c b/zlib/compress.c +index 6e976267..0aa12f0c 100644 +--- a/zlib/compress.c ++++ b/zlib/compress.c +@@ -19,12 +19,7 @@ + memory, Z_BUF_ERROR if there was not enough room in the output buffer, + Z_STREAM_ERROR if the level parameter is invalid. + */ +-int ZEXPORT compress2 (dest, destLen, source, sourceLen, level) +- Bytef *dest; +- uLongf *destLen; +- const Bytef *source; +- uLong sourceLen; +- int level; ++int ZEXPORT compress2 (Bytef *dest, uLongf *destLen, const Bytef *source, uLong sourceLen, int level) + { + z_stream stream; + int err; +@@ -59,11 +54,7 @@ int ZEXPORT compress2 (dest, destLen, source, sourceLen, level) + + /* =========================================================================== + */ +-int ZEXPORT compress (dest, destLen, source, sourceLen) +- Bytef *dest; +- uLongf *destLen; +- const Bytef *source; +- uLong sourceLen; ++int ZEXPORT compress (Bytef *dest, uLongf *destLen, const Bytef *source, uLong sourceLen) + { + return compress2(dest, destLen, source, sourceLen, Z_DEFAULT_COMPRESSION); + } +@@ -72,8 +63,7 @@ int ZEXPORT compress (dest, destLen, source, sourceLen) + If the default memLevel or windowBits for deflateInit() is changed, then + this function needs to be updated. + */ +-uLong ZEXPORT compressBound (sourceLen) +- uLong sourceLen; ++uLong ZEXPORT compressBound (uLong sourceLen) + { + return sourceLen + (sourceLen >> 12) + (sourceLen >> 14) + + (sourceLen >> 25) + 13; +diff --git a/zlib/crc32.c b/zlib/crc32.c +index 05733f4e..1d4cc44d 100644 +--- a/zlib/crc32.c ++++ b/zlib/crc32.c +@@ -87,7 +87,7 @@ local void make_crc_table OF((void)); + allow for word-at-a-time CRC calculation for both big-endian and little- + endian machines, where a word is four bytes. + */ +-local void make_crc_table() ++local void make_crc_table(void) + { + z_crc_t c; + int n, k; +@@ -164,9 +164,7 @@ local void make_crc_table() + } + + #ifdef MAKECRCH +-local void write_table(out, table) +- FILE *out; +- const z_crc_t FAR *table; ++local void write_table(FILE *out, const z_crc_t FAR *table) + { + int n; + +@@ -187,7 +185,7 @@ local void write_table(out, table) + /* ========================================================================= + * This function can be used by asm versions of crc32() + */ +-const z_crc_t FAR * ZEXPORT get_crc_table() ++const z_crc_t FAR * ZEXPORT get_crc_table(void) + { + #ifdef DYNAMIC_CRC_TABLE + if (crc_table_empty) +@@ -201,10 +199,7 @@ const z_crc_t FAR * ZEXPORT get_crc_table() + #define DO8 DO1; DO1; DO1; DO1; DO1; DO1; DO1; DO1 + + /* ========================================================================= */ +-unsigned long ZEXPORT crc32(crc, buf, len) +- unsigned long crc; +- const unsigned char FAR *buf; +- uInt len; ++unsigned long ZEXPORT crc32(unsigned long crc, const unsigned char FAR *buf, uInt len) + { + if (buf == Z_NULL) return 0UL; + +@@ -244,10 +239,7 @@ unsigned long ZEXPORT crc32(crc, buf, len) + #define DOLIT32 DOLIT4; DOLIT4; DOLIT4; DOLIT4; DOLIT4; DOLIT4; DOLIT4; DOLIT4 + + /* ========================================================================= */ +-local unsigned long crc32_little(crc, buf, len) +- unsigned long crc; +- const unsigned char FAR *buf; +- unsigned len; ++local unsigned long crc32_little(unsigned long crc, const unsigned char FAR *buf, unsigned len) + { + register z_crc_t c; + register const z_crc_t FAR *buf4; +@@ -284,10 +276,7 @@ local unsigned long crc32_little(crc, buf, len) + #define DOBIG32 DOBIG4; DOBIG4; DOBIG4; DOBIG4; DOBIG4; DOBIG4; DOBIG4; DOBIG4 + + /* ========================================================================= */ +-local unsigned long crc32_big(crc, buf, len) +- unsigned long crc; +- const unsigned char FAR *buf; +- unsigned len; ++local unsigned long crc32_big(unsigned long crc, const unsigned char FAR *buf, unsigned len) + { + register z_crc_t c; + register const z_crc_t FAR *buf4; +@@ -322,9 +311,7 @@ local unsigned long crc32_big(crc, buf, len) + #define GF2_DIM 32 /* dimension of GF(2) vectors (length of CRC) */ + + /* ========================================================================= */ +-local unsigned long gf2_matrix_times(mat, vec) +- unsigned long *mat; +- unsigned long vec; ++local unsigned long gf2_matrix_times(unsigned long *mat, unsigned long vec) + { + unsigned long sum; + +@@ -339,9 +326,7 @@ local unsigned long gf2_matrix_times(mat, vec) + } + + /* ========================================================================= */ +-local void gf2_matrix_square(square, mat) +- unsigned long *square; +- unsigned long *mat; ++local void gf2_matrix_square(unsigned long *square, unsigned long *mat) + { + int n; + +@@ -350,10 +335,7 @@ local void gf2_matrix_square(square, mat) + } + + /* ========================================================================= */ +-local uLong crc32_combine_(crc1, crc2, len2) +- uLong crc1; +- uLong crc2; +- z_off64_t len2; ++local uLong crc32_combine_(uLong crc1, uLong crc2, z_off64_t len2) + { + int n; + unsigned long row; +@@ -406,18 +388,12 @@ local uLong crc32_combine_(crc1, crc2, len2) + } + + /* ========================================================================= */ +-uLong ZEXPORT crc32_combine(crc1, crc2, len2) +- uLong crc1; +- uLong crc2; +- z_off_t len2; ++uLong ZEXPORT crc32_combine(uLong crc1, uLong crc2, z_off_t len2) + { + return crc32_combine_(crc1, crc2, len2); + } + +-uLong ZEXPORT crc32_combine64(crc1, crc2, len2) +- uLong crc1; +- uLong crc2; +- z_off64_t len2; ++uLong ZEXPORT crc32_combine64(uLong crc1, uLong crc2, z_off64_t len2) + { + return crc32_combine_(crc1, crc2, len2); + } +diff --git a/zlib/deflate.c b/zlib/deflate.c +index 2cbc4fc2..52fc3c04 100644 +--- a/zlib/deflate.c ++++ b/zlib/deflate.c +@@ -200,11 +200,8 @@ struct static_tree_desc_s {int dummy;}; /* for buggy compilers */ + zmemzero((Bytef *)s->head, (unsigned)(s->hash_size-1)*sizeof(*s->head)); + + /* ========================================================================= */ +-int ZEXPORT deflateInit_(strm, level, version, stream_size) +- z_streamp strm; +- int level; +- const char *version; +- int stream_size; ++int ZEXPORT deflateInit_(z_streamp strm, int level, const char *version, ++ int stream_size) + { + return deflateInit2_(strm, level, Z_DEFLATED, MAX_WBITS, DEF_MEM_LEVEL, + Z_DEFAULT_STRATEGY, version, stream_size); +@@ -212,16 +209,8 @@ int ZEXPORT deflateInit_(strm, level, version, stream_size) + } + + /* ========================================================================= */ +-int ZEXPORT deflateInit2_(strm, level, method, windowBits, memLevel, strategy, +- version, stream_size) +- z_streamp strm; +- int level; +- int method; +- int windowBits; +- int memLevel; +- int strategy; +- const char *version; +- int stream_size; ++int ZEXPORT deflateInit2_(z_streamp strm, int level, int method, int windowBits, ++ int memLevel, int strategy, const char *version, int stream_size) + { + deflate_state *s; + int wrap = 1; +@@ -359,10 +348,8 @@ int ZEXPORT deflateInit2_(strm, level, method, windowBits, memLevel, strategy, + } + + /* ========================================================================= */ +-int ZEXPORT deflateSetDictionary (strm, dictionary, dictLength) +- z_streamp strm; +- const Bytef *dictionary; +- uInt dictLength; ++int ZEXPORT deflateSetDictionary (z_streamp strm, const Bytef *dictionary, ++ uInt dictLength) + { + deflate_state *s; + uInt str, n; +@@ -428,8 +415,7 @@ int ZEXPORT deflateSetDictionary (strm, dictionary, dictLength) + } + + /* ========================================================================= */ +-int ZEXPORT deflateResetKeep (strm) +- z_streamp strm; ++int ZEXPORT deflateResetKeep (z_streamp strm) + { + deflate_state *s; + +@@ -463,8 +449,7 @@ int ZEXPORT deflateResetKeep (strm) + } + + /* ========================================================================= */ +-int ZEXPORT deflateReset (strm) +- z_streamp strm; ++int ZEXPORT deflateReset (z_streamp strm) + { + int ret; + +@@ -475,9 +460,7 @@ int ZEXPORT deflateReset (strm) + } + + /* ========================================================================= */ +-int ZEXPORT deflateSetHeader (strm, head) +- z_streamp strm; +- gz_headerp head; ++int ZEXPORT deflateSetHeader (z_streamp strm, gz_headerp head) + { + if (strm == Z_NULL || strm->state == Z_NULL) return Z_STREAM_ERROR; + if (strm->state->wrap != 2) return Z_STREAM_ERROR; +@@ -486,10 +469,7 @@ int ZEXPORT deflateSetHeader (strm, head) + } + + /* ========================================================================= */ +-int ZEXPORT deflatePending (strm, pending, bits) +- unsigned *pending; +- int *bits; +- z_streamp strm; ++int ZEXPORT deflatePending (z_streamp strm, unsigned *pending, int *bits) + { + if (strm == Z_NULL || strm->state == Z_NULL) return Z_STREAM_ERROR; + if (pending != Z_NULL) +@@ -500,10 +480,7 @@ int ZEXPORT deflatePending (strm, pending, bits) + } + + /* ========================================================================= */ +-int ZEXPORT deflatePrime (strm, bits, value) +- z_streamp strm; +- int bits; +- int value; ++int ZEXPORT deflatePrime (z_streamp strm, int bits, int value) + { + deflate_state *s; + int put; +@@ -526,10 +503,7 @@ int ZEXPORT deflatePrime (strm, bits, value) + } + + /* ========================================================================= */ +-int ZEXPORT deflateParams(strm, level, strategy) +- z_streamp strm; +- int level; +- int strategy; ++int ZEXPORT deflateParams(z_streamp strm, int level, int strategy) + { + deflate_state *s; + compress_func func; +@@ -567,12 +541,8 @@ int ZEXPORT deflateParams(strm, level, strategy) + } + + /* ========================================================================= */ +-int ZEXPORT deflateTune(strm, good_length, max_lazy, nice_length, max_chain) +- z_streamp strm; +- int good_length; +- int max_lazy; +- int nice_length; +- int max_chain; ++int ZEXPORT deflateTune(z_streamp strm, int good_length, int max_lazy, ++ int nice_length, int max_chain) + { + deflate_state *s; + +@@ -602,9 +572,7 @@ int ZEXPORT deflateTune(strm, good_length, max_lazy, nice_length, max_chain) + * upper bound of about 14% expansion does not seem onerous for output buffer + * allocation. + */ +-uLong ZEXPORT deflateBound(strm, sourceLen) +- z_streamp strm; +- uLong sourceLen; ++uLong ZEXPORT deflateBound(z_streamp strm, uLong sourceLen) + { + deflate_state *s; + uLong complen, wraplen; +@@ -664,9 +632,7 @@ uLong ZEXPORT deflateBound(strm, sourceLen) + * IN assertion: the stream state is correct and there is enough room in + * pending_buf. + */ +-local void putShortMSB (s, b) +- deflate_state *s; +- uInt b; ++local void putShortMSB (deflate_state *s, uInt b) + { + put_byte(s, (Byte)(b >> 8)); + put_byte(s, (Byte)(b & 0xff)); +@@ -678,8 +644,7 @@ local void putShortMSB (s, b) + * to avoid allocating a large strm->next_out buffer and copying into it. + * (See also read_buf()). + */ +-local void flush_pending(strm) +- z_streamp strm; ++local void flush_pending(z_streamp strm) + { + unsigned len; + deflate_state *s = strm->state; +@@ -701,9 +666,7 @@ local void flush_pending(strm) + } + + /* ========================================================================= */ +-int ZEXPORT deflate (strm, flush) +- z_streamp strm; +- int flush; ++int ZEXPORT deflate (z_streamp strm, int flush) + { + int old_flush; /* value of flush param for previous deflate call */ + deflate_state *s; +@@ -1015,8 +978,7 @@ int ZEXPORT deflate (strm, flush) + } + + /* ========================================================================= */ +-int ZEXPORT deflateEnd (strm) +- z_streamp strm; ++int ZEXPORT deflateEnd (z_streamp strm) + { + int status; + +@@ -1050,9 +1012,7 @@ int ZEXPORT deflateEnd (strm) + * To simplify the source, this is not supported for 16-bit MSDOS (which + * doesn't have enough memory anyway to duplicate compression states). + */ +-int ZEXPORT deflateCopy (dest, source) +- z_streamp dest; +- z_streamp source; ++int ZEXPORT deflateCopy (z_streamp dest, z_streamp source) + { + #ifdef MAXSEG_64K + return Z_STREAM_ERROR; +@@ -1109,10 +1069,7 @@ int ZEXPORT deflateCopy (dest, source) + * allocating a large strm->next_in buffer and copying from it. + * (See also flush_pending()). + */ +-local int read_buf(strm, buf, size) +- z_streamp strm; +- Bytef *buf; +- unsigned size; ++local int read_buf(z_streamp strm, Bytef *buf, unsigned size) + { + unsigned len = strm->avail_in; + +@@ -1139,8 +1096,7 @@ local int read_buf(strm, buf, size) + /* =========================================================================== + * Initialize the "longest match" routines for a new zlib stream + */ +-local void lm_init (s) +- deflate_state *s; ++local void lm_init (deflate_state *s) + { + s->window_size = (ulg)2L*s->w_size; + +@@ -1181,9 +1137,7 @@ local void lm_init (s) + /* For 80x86 and 680x0, an optimized version will be provided in match.asm or + * match.S. The code will be functionally equivalent. + */ +-local uInt longest_match(s, cur_match) +- deflate_state *s; +- IPos cur_match; /* current match */ ++local uInt longest_match(deflate_state *s, IPos cur_match) + { + unsigned chain_length = s->max_chain_length;/* max hash chain length */ + register Bytef *scan = s->window + s->strstart; /* current string */ +@@ -1330,9 +1284,7 @@ local uInt longest_match(s, cur_match) + /* --------------------------------------------------------------------------- + * Optimized version for FASTEST only + */ +-local uInt longest_match(s, cur_match) +- deflate_state *s; +- IPos cur_match; /* current match */ ++local uInt longest_match(deflate_state *s, IPos cur_match) + { + register Bytef *scan = s->window + s->strstart; /* current string */ + register Bytef *match; /* matched string */ +@@ -1389,10 +1341,7 @@ local uInt longest_match(s, cur_match) + /* =========================================================================== + * Check that the match at match_start is indeed a match. + */ +-local void check_match(s, start, match, length) +- deflate_state *s; +- IPos start, match; +- int length; ++local void check_match(deflate_state *s, IPos start, IPos match, int length) + { + /* check that the match is indeed a match */ + if (zmemcmp(s->window + match, +@@ -1423,8 +1372,7 @@ local void check_match(s, start, match, length) + * performed for at least two bytes (required for the zip translate_eol + * option -- not supported here). + */ +-local void fill_window(s) +- deflate_state *s; ++local void fill_window(deflate_state *s) + { + register unsigned n, m; + register Posf *p; +@@ -1597,9 +1545,7 @@ local void fill_window(s) + * NOTE: this function should be optimized to avoid extra copying from + * window to pending_buf. + */ +-local block_state deflate_stored(s, flush) +- deflate_state *s; +- int flush; ++local block_state deflate_stored(deflate_state *s, int flush) + { + /* Stored blocks are limited to 0xffff bytes, pending_buf is limited + * to pending_buf_size, and each stored block has a 5 byte header: +@@ -1670,9 +1616,7 @@ local block_state deflate_stored(s, flush) + * new strings in the dictionary only for unmatched strings or for short + * matches. It is used only for the fast compression options. + */ +-local block_state deflate_fast(s, flush) +- deflate_state *s; +- int flush; ++local block_state deflate_fast(deflate_state *s, int flush) + { + IPos hash_head; /* head of the hash chain */ + int bflush; /* set if current block must be flushed */ +@@ -1782,9 +1726,7 @@ local block_state deflate_fast(s, flush) + * evaluation for matches: a match is finally adopted only if there is + * no better match at the next window position. + */ +-local block_state deflate_slow(s, flush) +- deflate_state *s; +- int flush; ++local block_state deflate_slow(deflate_state *s, int flush) + { + IPos hash_head; /* head of hash chain */ + int bflush; /* set if current block must be flushed */ +@@ -1923,9 +1865,7 @@ local block_state deflate_slow(s, flush) + * one. Do not maintain a hash table. (It will be regenerated if this run of + * deflate switches away from Z_RLE.) + */ +-local block_state deflate_rle(s, flush) +- deflate_state *s; +- int flush; ++local block_state deflate_rle(deflate_state *s, int flush) + { + int bflush; /* set if current block must be flushed */ + uInt prev; /* byte at distance one to match */ +@@ -1996,9 +1936,7 @@ local block_state deflate_rle(s, flush) + * For Z_HUFFMAN_ONLY, do not look for matches. Do not maintain a hash table. + * (It will be regenerated if this run of deflate switches away from Huffman.) + */ +-local block_state deflate_huff(s, flush) +- deflate_state *s; +- int flush; ++local block_state deflate_huff(deflate_state *s, int flush) + { + int bflush; /* set if current block must be flushed */ + +diff --git a/zlib/inffast.c b/zlib/inffast.c +index f0d163db..264291ae 100644 +--- a/zlib/inffast.c ++++ b/zlib/inffast.c +@@ -45,9 +45,7 @@ + requires strm->avail_out >= 258 for each loop to avoid checking for + output space. + */ +-void ZLIB_INTERNAL inflate_fast(strm, start) +-z_streamp strm; +-unsigned start; /* inflate()'s starting value for strm->avail_out */ ++void ZLIB_INTERNAL inflate_fast(z_streamp strm, unsigned start) + { + struct inflate_state FAR *state; + z_const unsigned char FAR *in; /* local strm->next_in */ +diff --git a/zlib/inflate.c b/zlib/inflate.c +index e9840b67..1afe007a 100644 +--- a/zlib/inflate.c ++++ b/zlib/inflate.c +@@ -101,8 +101,7 @@ local int updatewindow OF((z_streamp strm, const unsigned char FAR *end, + local unsigned syncsearch OF((unsigned FAR *have, const unsigned char FAR *buf, + unsigned len)); + +-int ZEXPORT inflateResetKeep(strm) +-z_streamp strm; ++int ZEXPORT inflateResetKeep(z_streamp strm) + { + struct inflate_state FAR *state; + +@@ -126,8 +125,7 @@ z_streamp strm; + return Z_OK; + } + +-int ZEXPORT inflateReset(strm) +-z_streamp strm; ++int ZEXPORT inflateReset(z_streamp strm) + { + struct inflate_state FAR *state; + +@@ -139,9 +137,7 @@ z_streamp strm; + return inflateResetKeep(strm); + } + +-int ZEXPORT inflateReset2(strm, windowBits) +-z_streamp strm; +-int windowBits; ++int ZEXPORT inflateReset2(z_streamp strm, int windowBits) + { + int wrap; + struct inflate_state FAR *state; +@@ -177,11 +173,7 @@ int windowBits; + return inflateReset(strm); + } + +-int ZEXPORT inflateInit2_(strm, windowBits, version, stream_size) +-z_streamp strm; +-int windowBits; +-const char *version; +-int stream_size; ++int ZEXPORT inflateInit2_(z_streamp strm, int windowBits, const char *version, int stream_size) + { + int ret; + struct inflate_state FAR *state; +@@ -219,18 +211,12 @@ int stream_size; + return ret; + } + +-int ZEXPORT inflateInit_(strm, version, stream_size) +-z_streamp strm; +-const char *version; +-int stream_size; ++int ZEXPORT inflateInit_(z_streamp strm, const char *version, int stream_size) + { + return inflateInit2_(strm, DEF_WBITS, version, stream_size); + } + +-int ZEXPORT inflatePrime(strm, bits, value) +-z_streamp strm; +-int bits; +-int value; ++int ZEXPORT inflatePrime(z_streamp strm, int bits, int value) + { + struct inflate_state FAR *state; + +@@ -258,8 +244,7 @@ int value; + used for threaded applications, since the rewriting of the tables and virgin + may not be thread-safe. + */ +-local void fixedtables(state) +-struct inflate_state FAR *state; ++local void fixedtables(struct inflate_state FAR *state) + { + #ifdef BUILDFIXED + static int virgin = 1; +@@ -376,10 +361,7 @@ void makefixed() + output will fall in the output data, making match copies simpler and faster. + The advantage may be dependent on the size of the processor's data caches. + */ +-local int updatewindow(strm, end, copy) +-z_streamp strm; +-const Bytef *end; +-unsigned copy; ++local int updatewindow(z_streamp strm, const Bytef *end, unsigned copy) + { + struct inflate_state FAR *state; + unsigned dist; +@@ -602,9 +584,7 @@ unsigned copy; + will return Z_BUF_ERROR if it has not reached the end of the stream. + */ + +-int ZEXPORT inflate(strm, flush) +-z_streamp strm; +-int flush; ++int ZEXPORT inflate(z_streamp strm, int flush) + { + struct inflate_state FAR *state; + z_const unsigned char FAR *next; /* next input */ +@@ -1274,8 +1254,7 @@ int flush; + return ret; + } + +-int ZEXPORT inflateEnd(strm) +-z_streamp strm; ++int ZEXPORT inflateEnd(z_streamp strm) + { + struct inflate_state FAR *state; + if (strm == Z_NULL || strm->state == Z_NULL || strm->zfree == (free_func)0) +@@ -1288,10 +1267,7 @@ z_streamp strm; + return Z_OK; + } + +-int ZEXPORT inflateGetDictionary(strm, dictionary, dictLength) +-z_streamp strm; +-Bytef *dictionary; +-uInt *dictLength; ++int ZEXPORT inflateGetDictionary(z_streamp strm, Bytef *dictionary, uInt *dictLength) + { + struct inflate_state FAR *state; + +@@ -1311,10 +1287,7 @@ uInt *dictLength; + return Z_OK; + } + +-int ZEXPORT inflateSetDictionary(strm, dictionary, dictLength) +-z_streamp strm; +-const Bytef *dictionary; +-uInt dictLength; ++int ZEXPORT inflateSetDictionary(z_streamp strm, const Bytef *dictionary, uInt dictLength) + { + struct inflate_state FAR *state; + unsigned long dictid; +@@ -1346,9 +1319,7 @@ uInt dictLength; + return Z_OK; + } + +-int ZEXPORT inflateGetHeader(strm, head) +-z_streamp strm; +-gz_headerp head; ++int ZEXPORT inflateGetHeader(z_streamp strm, gz_headerp head) + { + struct inflate_state FAR *state; + +@@ -1374,10 +1345,7 @@ gz_headerp head; + called again with more data and the *have state. *have is initialized to + zero for the first call. + */ +-local unsigned syncsearch(have, buf, len) +-unsigned FAR *have; +-const unsigned char FAR *buf; +-unsigned len; ++local unsigned syncsearch(unsigned FAR *have, const unsigned char FAR *buf, unsigned len) + { + unsigned got; + unsigned next; +@@ -1397,8 +1365,7 @@ unsigned len; + return next; + } + +-int ZEXPORT inflateSync(strm) +-z_streamp strm; ++int ZEXPORT inflateSync(z_streamp strm) + { + unsigned len; /* number of bytes to look at or looked at */ + unsigned long in, out; /* temporary to save total_in and total_out */ +@@ -1448,8 +1415,7 @@ z_streamp strm; + block. When decompressing, PPP checks that at the end of input packet, + inflate is waiting for these length bytes. + */ +-int ZEXPORT inflateSyncPoint(strm) +-z_streamp strm; ++int ZEXPORT inflateSyncPoint(z_streamp strm) + { + struct inflate_state FAR *state; + +@@ -1458,9 +1424,7 @@ z_streamp strm; + return state->mode == STORED && state->bits == 0; + } + +-int ZEXPORT inflateCopy(dest, source) +-z_streamp dest; +-z_streamp source; ++int ZEXPORT inflateCopy(z_streamp dest, z_streamp source) + { + struct inflate_state FAR *state; + struct inflate_state FAR *copy; +@@ -1505,9 +1469,7 @@ z_streamp source; + return Z_OK; + } + +-int ZEXPORT inflateUndermine(strm, subvert) +-z_streamp strm; +-int subvert; ++int ZEXPORT inflateUndermine(z_streamp strm, int subvert) + { + struct inflate_state FAR *state; + +@@ -1522,8 +1484,7 @@ int subvert; + #endif + } + +-long ZEXPORT inflateMark(strm) +-z_streamp strm; ++long ZEXPORT inflateMark(z_streamp strm) + { + struct inflate_state FAR *state; + +diff --git a/zlib/inftrees.c b/zlib/inftrees.c +index 571e8100..bea5789a 100644 +--- a/zlib/inftrees.c ++++ b/zlib/inftrees.c +@@ -29,13 +29,9 @@ const char inflate_copyright[] = + table index bits. It will differ if the request is greater than the + longest code or if it is less than the shortest code. + */ +-int ZLIB_INTERNAL inflate_table(type, lens, codes, table, bits, work) +-codetype type; +-unsigned short FAR *lens; +-unsigned codes; +-code FAR * FAR *table; +-unsigned FAR *bits; +-unsigned short FAR *work; ++int ZLIB_INTERNAL inflate_table(codetype type, unsigned short FAR *lens, ++ unsigned codes, code FAR * FAR *table, unsigned FAR *bits, ++ unsigned short FAR *work) + { + unsigned len; /* a code's length in bits */ + unsigned sym; /* index of code symbols */ +diff --git a/zlib/trees.c b/zlib/trees.c +index 9c667702..0f7a5cd9 100644 +--- a/zlib/trees.c ++++ b/zlib/trees.c +@@ -185,10 +185,7 @@ local void gen_trees_header OF((void)); + #ifdef DEBUG + local void send_bits OF((deflate_state *s, int value, int length)); + +-local void send_bits(s, value, length) +- deflate_state *s; +- int value; /* value to send */ +- int length; /* number of bits */ ++local void send_bits(deflate_state *s, int value, int length) + { + Tracevv((stderr," l %2d v %4x ", length, value)); + Assert(length > 0 && length <= 15, "invalid length"); +@@ -231,7 +228,7 @@ local void send_bits(s, value, length) + /* =========================================================================== + * Initialize the various 'constant' tables. + */ +-local void tr_static_init() ++local void tr_static_init(void) + { + #if defined(GEN_TREES_H) || !defined(STDC) + static int static_init_done = 0; +@@ -325,7 +322,7 @@ local void tr_static_init() + ((i) == (last)? "\n};\n\n" : \ + ((i) % (width) == (width)-1 ? ",\n" : ", ")) + +-void gen_trees_header() ++void gen_trees_header(void) + { + FILE *header = fopen("trees.h", "w"); + int i; +@@ -378,8 +375,7 @@ void gen_trees_header() + /* =========================================================================== + * Initialize the tree data structures for a new zlib stream. + */ +-void ZLIB_INTERNAL _tr_init(s) +- deflate_state *s; ++void ZLIB_INTERNAL _tr_init(deflate_state *s) + { + tr_static_init(); + +@@ -406,8 +402,7 @@ void ZLIB_INTERNAL _tr_init(s) + /* =========================================================================== + * Initialize a new block. + */ +-local void init_block(s) +- deflate_state *s; ++local void init_block(deflate_state *s) + { + int n; /* iterates over tree elements */ + +@@ -450,10 +445,7 @@ local void init_block(s) + * when the heap property is re-established (each father smaller than its + * two sons). + */ +-local void pqdownheap(s, tree, k) +- deflate_state *s; +- ct_data *tree; /* the tree to restore */ +- int k; /* node to move down */ ++local void pqdownheap(deflate_state *s, ct_data *tree, int k) + { + int v = s->heap[k]; + int j = k << 1; /* left son of k */ +@@ -485,9 +477,7 @@ local void pqdownheap(s, tree, k) + * The length opt_len is updated; static_len is also updated if stree is + * not null. + */ +-local void gen_bitlen(s, desc) +- deflate_state *s; +- tree_desc *desc; /* the tree descriptor */ ++local void gen_bitlen(deflate_state *s, tree_desc *desc) + { + ct_data *tree = desc->dyn_tree; + int max_code = desc->max_code; +@@ -572,10 +562,7 @@ local void gen_bitlen(s, desc) + * OUT assertion: the field code is set for all tree elements of non + * zero code length. + */ +-local void gen_codes (tree, max_code, bl_count) +- ct_data *tree; /* the tree to decorate */ +- int max_code; /* largest code with non zero frequency */ +- ushf *bl_count; /* number of codes at each bit length */ ++local void gen_codes(ct_data *tree, int max_code, ushf *bl_count) + { + ush next_code[MAX_BITS+1]; /* next code value for each bit length */ + ush code = 0; /* running code value */ +@@ -614,9 +601,7 @@ local void gen_codes (tree, max_code, bl_count) + * and corresponding code. The length opt_len is updated; static_len is + * also updated if stree is not null. The field max_code is set. + */ +-local void build_tree(s, desc) +- deflate_state *s; +- tree_desc *desc; /* the tree descriptor */ ++local void build_tree(deflate_state *s, tree_desc *desc) + { + ct_data *tree = desc->dyn_tree; + const ct_data *stree = desc->stat_desc->static_tree; +@@ -702,10 +687,7 @@ local void build_tree(s, desc) + * Scan a literal or distance tree to determine the frequencies of the codes + * in the bit length tree. + */ +-local void scan_tree (s, tree, max_code) +- deflate_state *s; +- ct_data *tree; /* the tree to be scanned */ +- int max_code; /* and its largest code of non zero frequency */ ++local void scan_tree(deflate_state *s, ct_data *tree, int max_code) + { + int n; /* iterates over all tree elements */ + int prevlen = -1; /* last emitted length */ +@@ -747,10 +729,7 @@ local void scan_tree (s, tree, max_code) + * Send a literal or distance tree in compressed form, using the codes in + * bl_tree. + */ +-local void send_tree (s, tree, max_code) +- deflate_state *s; +- ct_data *tree; /* the tree to be scanned */ +- int max_code; /* and its largest code of non zero frequency */ ++local void send_tree(deflate_state *s, ct_data *tree, int max_code) + { + int n; /* iterates over all tree elements */ + int prevlen = -1; /* last emitted length */ +@@ -798,8 +777,7 @@ local void send_tree (s, tree, max_code) + * Construct the Huffman tree for the bit lengths and return the index in + * bl_order of the last bit length code to send. + */ +-local int build_bl_tree(s) +- deflate_state *s; ++local int build_bl_tree(deflate_state *s) + { + int max_blindex; /* index of last bit length code of non zero freq */ + +@@ -833,9 +811,7 @@ local int build_bl_tree(s) + * lengths of the bit length codes, the literal tree and the distance tree. + * IN assertion: lcodes >= 257, dcodes >= 1, blcodes >= 4. + */ +-local void send_all_trees(s, lcodes, dcodes, blcodes) +- deflate_state *s; +- int lcodes, dcodes, blcodes; /* number of codes for each tree */ ++local void send_all_trees(deflate_state *s, int lcodes, int dcodes, int blcodes) + { + int rank; /* index in bl_order */ + +@@ -862,11 +838,7 @@ local void send_all_trees(s, lcodes, dcodes, blcodes) + /* =========================================================================== + * Send a stored block + */ +-void ZLIB_INTERNAL _tr_stored_block(s, buf, stored_len, last) +- deflate_state *s; +- charf *buf; /* input block */ +- ulg stored_len; /* length of input block */ +- int last; /* one if this is the last block for a file */ ++void ZLIB_INTERNAL _tr_stored_block(deflate_state *s, charf *buf, ulg stored_len, int last) + { + send_bits(s, (STORED_BLOCK<<1)+last, 3); /* send block type */ + #ifdef DEBUG +@@ -879,8 +851,7 @@ void ZLIB_INTERNAL _tr_stored_block(s, buf, stored_len, last) + /* =========================================================================== + * Flush the bits in the bit buffer to pending output (leaves at most 7 bits) + */ +-void ZLIB_INTERNAL _tr_flush_bits(s) +- deflate_state *s; ++void ZLIB_INTERNAL _tr_flush_bits(deflate_state *s) + { + bi_flush(s); + } +@@ -889,8 +860,7 @@ void ZLIB_INTERNAL _tr_flush_bits(s) + * Send one empty static block to give enough lookahead for inflate. + * This takes 10 bits, of which 7 may remain in the bit buffer. + */ +-void ZLIB_INTERNAL _tr_align(s) +- deflate_state *s; ++void ZLIB_INTERNAL _tr_align(deflate_state *s) + { + send_bits(s, STATIC_TREES<<1, 3); + send_code(s, END_BLOCK, static_ltree); +@@ -904,11 +874,7 @@ void ZLIB_INTERNAL _tr_align(s) + * Determine the best encoding for the current block: dynamic trees, static + * trees or store, and output the encoded block to the zip file. + */ +-void ZLIB_INTERNAL _tr_flush_block(s, buf, stored_len, last) +- deflate_state *s; +- charf *buf; /* input block, or NULL if too old */ +- ulg stored_len; /* length of input block */ +- int last; /* one if this is the last block for a file */ ++void ZLIB_INTERNAL _tr_flush_block(deflate_state *s, charf *buf, ulg stored_len, int last) + { + ulg opt_lenb, static_lenb; /* opt_len and static_len in bytes */ + int max_blindex = 0; /* index of last bit length code of non zero freq */ +@@ -1007,10 +973,7 @@ void ZLIB_INTERNAL _tr_flush_block(s, buf, stored_len, last) + * Save the match info and tally the frequency counts. Return true if + * the current block must be flushed. + */ +-int ZLIB_INTERNAL _tr_tally (s, dist, lc) +- deflate_state *s; +- unsigned dist; /* distance of matched string */ +- unsigned lc; /* match length-MIN_MATCH or unmatched char (if dist==0) */ ++int ZLIB_INTERNAL _tr_tally(deflate_state *s, unsigned dist, unsigned lc) + { + s->sym_buf[s->sym_next++] = dist; + s->sym_buf[s->sym_next++] = dist >> 8; +@@ -1035,10 +998,7 @@ int ZLIB_INTERNAL _tr_tally (s, dist, lc) + /* =========================================================================== + * Send the block data compressed using the given Huffman trees + */ +-local void compress_block(s, ltree, dtree) +- deflate_state *s; +- const ct_data *ltree; /* literal tree */ +- const ct_data *dtree; /* distance tree */ ++local void compress_block(deflate_state *s, const ct_data *ltree, const ct_data *dtree) + { + unsigned dist; /* distance of matched string */ + int lc; /* match length or unmatched char (if dist == 0) */ +@@ -1095,8 +1055,7 @@ local void compress_block(s, ltree, dtree) + * (7 {BEL}, 8 {BS}, 11 {VT}, 12 {FF}, 26 {SUB}, 27 {ESC}). + * IN assertion: the fields Freq of dyn_ltree are set. + */ +-local int detect_data_type(s) +- deflate_state *s; ++local int detect_data_type(deflate_state *s) + { + /* black_mask is the bit mask of black-listed bytes + * set bits 0..6, 14..25, and 28..31 +@@ -1129,9 +1088,7 @@ local int detect_data_type(s) + * method would use a table) + * IN assertion: 1 <= len <= 15 + */ +-local unsigned bi_reverse(code, len) +- unsigned code; /* the value to invert */ +- int len; /* its bit length */ ++local unsigned bi_reverse(unsigned code, int len) + { + register unsigned res = 0; + do { +@@ -1144,8 +1101,7 @@ local unsigned bi_reverse(code, len) + /* =========================================================================== + * Flush the bit buffer, keeping at most 7 bits in it. + */ +-local void bi_flush(s) +- deflate_state *s; ++local void bi_flush(deflate_state *s) + { + if (s->bi_valid == 16) { + put_short(s, s->bi_buf); +@@ -1161,8 +1117,7 @@ local void bi_flush(s) + /* =========================================================================== + * Flush the bit buffer and align the output on a byte boundary + */ +-local void bi_windup(s) +- deflate_state *s; ++local void bi_windup(deflate_state *s) + { + if (s->bi_valid > 8) { + put_short(s, s->bi_buf); +@@ -1180,11 +1135,7 @@ local void bi_windup(s) + * Copy a stored block, storing first the length and its + * one's complement if requested. + */ +-local void copy_block(s, buf, len, header) +- deflate_state *s; +- charf *buf; /* the input data */ +- unsigned len; /* its length */ +- int header; /* true if block header must be written */ ++local void copy_block(deflate_state *s, charf *buf, unsigned len, int header) + { + bi_windup(s); /* align on byte boundary */ + +diff --git a/zlib/zutil.c b/zlib/zutil.c +index bbba7b21..466605f7 100644 +--- a/zlib/zutil.c ++++ b/zlib/zutil.c +@@ -27,12 +27,12 @@ z_const char * const z_errmsg[10] = { + ""}; + + +-const char * ZEXPORT zlibVersion() ++const char * ZEXPORT zlibVersion(void) + { + return ZLIB_VERSION; + } + +-uLong ZEXPORT zlibCompileFlags() ++uLong ZEXPORT zlibCompileFlags(void) + { + uLong flags; + +@@ -122,8 +122,7 @@ uLong ZEXPORT zlibCompileFlags() + # endif + int ZLIB_INTERNAL z_verbose = verbose; + +-void ZLIB_INTERNAL z_error (m) +- char *m; ++void ZLIB_INTERNAL z_error (char *m) + { + fprintf(stderr, "%s\n", m); + exit(1); +@@ -133,8 +132,7 @@ void ZLIB_INTERNAL z_error (m) + /* exported to allow conversion of error code to string for compress() and + * uncompress() + */ +-const char * ZEXPORT zError(err) +- int err; ++const char * ZEXPORT zError(int err) + { + return ERR_MSG(err); + } +@@ -149,10 +147,7 @@ const char * ZEXPORT zError(err) + + #ifndef HAVE_MEMCPY + +-void ZLIB_INTERNAL zmemcpy(dest, source, len) +- Bytef* dest; +- const Bytef* source; +- uInt len; ++void ZLIB_INTERNAL zmemcpy(Bytef* dest, const Bytef* source, uInt len) + { + if (len == 0) return; + do { +@@ -160,10 +155,7 @@ void ZLIB_INTERNAL zmemcpy(dest, source, len) + } while (--len != 0); + } + +-int ZLIB_INTERNAL zmemcmp(s1, s2, len) +- const Bytef* s1; +- const Bytef* s2; +- uInt len; ++int ZLIB_INTERNAL zmemcmp(const Bytef* s1, const Bytef* s2, uInt len) + { + uInt j; + +@@ -173,9 +165,7 @@ int ZLIB_INTERNAL zmemcmp(s1, s2, len) + return 0; + } + +-void ZLIB_INTERNAL zmemzero(dest, len) +- Bytef* dest; +- uInt len; ++void ZLIB_INTERNAL zmemzero(Bytef* dest, uInt len) + { + if (len == 0) return; + do { +@@ -301,19 +291,14 @@ extern voidp calloc OF((uInt items, uInt size)); + extern void free OF((voidpf ptr)); + #endif + +-voidpf ZLIB_INTERNAL zcalloc (opaque, items, size) +- voidpf opaque; +- unsigned items; +- unsigned size; ++voidpf ZLIB_INTERNAL zcalloc (voidpf opaque, unsigned items, unsigned size) + { + if (opaque) items += size - size; /* make compiler happy */ + return sizeof(uInt) > 2 ? (voidpf)malloc(items * size) : + (voidpf)calloc(items, size); + } + +-void ZLIB_INTERNAL zcfree (opaque, ptr) +- voidpf opaque; +- voidpf ptr; ++void ZLIB_INTERNAL zcfree (voidpf opaque, voidpf ptr) + { + free(ptr); + if (opaque) return; /* make compiler happy */ +-- +2.51.0 + diff -Nru rsync-3.2.7/debian/patches/2026-05-20/0029-fix-signed-integer-overflow-in-proxy-protocol-v2-hea.patch rsync-3.2.7/debian/patches/2026-05-20/0029-fix-signed-integer-overflow-in-proxy-protocol-v2-hea.patch --- rsync-3.2.7/debian/patches/2026-05-20/0029-fix-signed-integer-overflow-in-proxy-protocol-v2-hea.patch 1970-01-01 00:00:00.000000000 +0000 +++ rsync-3.2.7/debian/patches/2026-05-20/0029-fix-signed-integer-overflow-in-proxy-protocol-v2-hea.patch 2026-05-20 05:47:50.000000000 +0000 @@ -0,0 +1,38 @@ +From cad6aab5347ea38f4106d48f4f8fe396041daa62 Mon Sep 17 00:00:00 2001 +From: Andrew Tridgell +Date: Thu, 16 Apr 2026 10:50:49 +1000 +Subject: [PATCH 29/56] fix signed integer overflow in proxy protocol v2 header + parsing + +The len field in the proxy v2 header was declared as signed char, +allowing a negative size to bypass the validation check and cause +a stack buffer overflow when passed to read_buf() as size_t. + +This bug was reported by John Walker from ZeroPath, many thanks for +the clear report! + +With the current code this bug does not represent a security issue as +it only results in the exit of the forked process that is specific to +the attached client, so it is equivalent to the client closing the +socket, so no CVE for this, but it is good to fix it to prevent a +future issue. +--- + clientname.c | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/clientname.c b/clientname.c +index ea94894b..dbac38b9 100644 +--- a/clientname.c ++++ b/clientname.c +@@ -167,7 +167,7 @@ int read_proxy_protocol_header(int fd) + char sig[PROXY_V2_SIG_SIZE]; + char ver_cmd; + char fam; +- char len[2]; ++ unsigned char len[2]; + union { + struct { + char src_addr[4]; +-- +2.51.0 + diff -Nru rsync-3.2.7/debian/patches/2026-05-20/0030-zero-all-new-memory-from-allocations.patch rsync-3.2.7/debian/patches/2026-05-20/0030-zero-all-new-memory-from-allocations.patch --- rsync-3.2.7/debian/patches/2026-05-20/0030-zero-all-new-memory-from-allocations.patch 1970-01-01 00:00:00.000000000 +0000 +++ rsync-3.2.7/debian/patches/2026-05-20/0030-zero-all-new-memory-from-allocations.patch 2026-05-20 05:47:50.000000000 +0000 @@ -0,0 +1,48 @@ +From d69162dbf46c88003584c195119b5793e18c8f48 Mon Sep 17 00:00:00 2001 +From: Andrew Tridgell +Date: Wed, 22 Apr 2026 10:59:11 +1000 +Subject: [PATCH 30/56] zero all new memory from allocations + +Change my_alloc() to use calloc instead of malloc so all fresh +allocations return zeroed memory. Also zero the expanded portion +in expand_item_list() after realloc, since it knows both old and +new sizes. This gives more predictable behaviour in case of bugs +where uninitialised or stale memory is accidentally accessed. + +Co-Authored-By: Claude Opus 4.6 (1M context) +--- + util1.c | 2 ++ + util2.c | 4 +--- + 2 files changed, 3 insertions(+), 3 deletions(-) + +diff --git a/util1.c b/util1.c +index de634a84..25ac7c9b 100644 +--- a/util1.c ++++ b/util1.c +@@ -1718,6 +1718,8 @@ void *expand_item_list(item_list *lp, size_t item_size, const char *desc, int in + new_ptr == lp->items ? " not" : ""); + } + ++ memset((char *)new_ptr + lp->malloced * item_size, 0, ++ (expand_size - lp->malloced) * item_size); + lp->items = new_ptr; + lp->malloced = expand_size; + } +diff --git a/util2.c b/util2.c +index a8609a5d..c241cfb7 100644 +--- a/util2.c ++++ b/util2.c +@@ -79,9 +79,7 @@ void *my_alloc(void *ptr, size_t num, size_t size, const char *file, int line) + who_am_i(), do_big_num(max_alloc, 0, NULL), src_file(file), line); + exit_cleanup(RERR_MALLOC); + } +- if (!ptr) +- ptr = malloc(num * size); +- else if (ptr == do_calloc) ++ if (!ptr || ptr == do_calloc) + ptr = calloc(num, size); + else + ptr = realloc(ptr, num * size); +-- +2.51.0 + diff -Nru rsync-3.2.7/debian/patches/2026-05-20/0031-xattrs-fixed-count-in-qsort.patch rsync-3.2.7/debian/patches/2026-05-20/0031-xattrs-fixed-count-in-qsort.patch --- rsync-3.2.7/debian/patches/2026-05-20/0031-xattrs-fixed-count-in-qsort.patch 1970-01-01 00:00:00.000000000 +0000 +++ rsync-3.2.7/debian/patches/2026-05-20/0031-xattrs-fixed-count-in-qsort.patch 2026-05-20 05:47:50.000000000 +0000 @@ -0,0 +1,35 @@ +From 968d4c0cd0cf7f24712547a24f8f04e1311a1ca7 Mon Sep 17 00:00:00 2001 +From: Andrew Tridgell +Date: Wed, 22 Apr 2026 09:57:45 +1000 +Subject: [PATCH 31/56] xattrs: fixed count in qsort + +this fixes the count passed to the sort of the xattr list. This issue +was reported here: + +https://www.openwall.com/lists/oss-security/2026/04/16/2 + +the bug is not exploitable due to the fork-per-connection design of +rsync, the attack is the equivalent of the user closing the socket +themselves. +--- + xattrs.c | 4 ++-- + 1 file changed, 2 insertions(+), 2 deletions(-) + +diff --git a/xattrs.c b/xattrs.c +index 26e50a6f..65166eed 100644 +--- a/xattrs.c ++++ b/xattrs.c +@@ -860,8 +860,8 @@ void receive_xattr(int f, struct file_struct *file) + rxa->num = num; + } + +- if (need_sort && count > 1) +- qsort(temp_xattr.items, count, sizeof (rsync_xa), rsync_xal_compare_names); ++ if (need_sort && temp_xattr.count > 1) ++ qsort(temp_xattr.items, temp_xattr.count, sizeof (rsync_xa), rsync_xal_compare_names); + + ndx = rsync_xal_store(&temp_xattr); /* adds item to rsync_xal_l */ + +-- +2.51.0 + diff -Nru rsync-3.2.7/debian/patches/2026-05-20/0032-call-tzset-before-chroot-to-cache-timezone-data.patch rsync-3.2.7/debian/patches/2026-05-20/0032-call-tzset-before-chroot-to-cache-timezone-data.patch --- rsync-3.2.7/debian/patches/2026-05-20/0032-call-tzset-before-chroot-to-cache-timezone-data.patch 1970-01-01 00:00:00.000000000 +0000 +++ rsync-3.2.7/debian/patches/2026-05-20/0032-call-tzset-before-chroot-to-cache-timezone-data.patch 2026-05-20 05:47:50.000000000 +0000 @@ -0,0 +1,39 @@ +From acf4b7b83933abf6ff53ee3c4664cbbf7a9adab1 Mon Sep 17 00:00:00 2001 +From: Andrew Tridgell +Date: Wed, 22 Apr 2026 12:53:13 +1000 +Subject: [PATCH 32/56] call tzset() before chroot to cache timezone data + +localtime/localtime_r need /etc/localtime for timezone info. +After chroot this file is inaccessible, causing log timestamps +to fall back to UTC. Calling tzset() before chroot ensures the +timezone data is cached by glibc for subsequent calls. + +Co-Authored-By: Claude Opus 4.6 (1M context) +--- + clientserver.c | 3 +++ + 1 file changed, 3 insertions(+) + +diff --git a/clientserver.c b/clientserver.c +index 7c897abc..3800f0d6 100644 +--- a/clientserver.c ++++ b/clientserver.c +@@ -976,6 +976,8 @@ static int rsync_module(int f_in, int f_out, int i, const char *addr, const char + } + + if (use_chroot) { ++ /* Cache timezone data before chroot makes /etc/localtime inaccessible */ ++ tzset(); + if (chroot(module_chdir)) { + rsyserr(FLOG, errno, "chroot(\"%s\") failed", module_chdir); + io_printf(f_out, "@ERROR: chroot failed\n"); +@@ -1301,6 +1303,7 @@ int start_daemon(int f_in, int f_out) + p = lp_daemon_chroot(); + if (*p) { + log_init(0); /* Make use we've initialized syslog before chrooting. */ ++ tzset(); + if (chroot(p) < 0) { + rsyserr(FLOG, errno, "daemon chroot(\"%s\") failed", p); + return -1; +-- +2.51.0 + diff -Nru rsync-3.2.7/debian/patches/2026-05-20/0033-testsuite-xattrs-ignore-SUNWattr_-in-the-Solaris-xls.patch rsync-3.2.7/debian/patches/2026-05-20/0033-testsuite-xattrs-ignore-SUNWattr_-in-the-Solaris-xls.patch --- rsync-3.2.7/debian/patches/2026-05-20/0033-testsuite-xattrs-ignore-SUNWattr_-in-the-Solaris-xls.patch 1970-01-01 00:00:00.000000000 +0000 +++ rsync-3.2.7/debian/patches/2026-05-20/0033-testsuite-xattrs-ignore-SUNWattr_-in-the-Solaris-xls.patch 2026-05-20 05:47:50.000000000 +0000 @@ -0,0 +1,45 @@ +From 9ba21f638c078a5e1ecc799f8aec17755a40d389 Mon Sep 17 00:00:00 2001 +From: Andrew Tridgell +Date: Thu, 30 Apr 2026 08:18:01 +1000 +Subject: [PATCH 33/56] testsuite/xattrs: ignore SUNWattr_* in the Solaris xls + helper + +The Solaris xls() function listed every entry in the file's xattr +directory, which on Solaris includes OS-managed SUNWattr_ro and +SUNWattr_rw pseudo-attributes. SUNWattr_rw embeds the file creation +time, so its bytes naturally differ between the source and destination +files, making the xattrs and xattrs-hlink tests fail with diffs that +have nothing to do with rsync. + +Rsync's own listxattr wrapper already filters these out +(lib/sysxattrs.c), so the right fix is to filter them in the test +display too. Other platforms are unaffected because each has its own +xls() branch in the case statement. + +With the test now actually passing on Solaris, drop the CI hack that +overwrote testsuite/xattrs.test with a skip stub. + +Co-Authored-By: Claude Opus 4.7 (1M context) +--- + testsuite/xattrs.test | 5 ++++- + 1 file changed, 4 insertions(+), 1 deletion(-) + +diff --git a/testsuite/xattrs.test b/testsuite/xattrs.test +index d94d5f95..c0f3784b 100644 +--- a/testsuite/xattrs.test ++++ b/testsuite/xattrs.test +@@ -38,7 +38,10 @@ EOF + xls() { + for fn in "${@}"; do + runat "$fn" "$SHELL_PATH" < +Date: Thu, 30 Apr 2026 08:39:22 +1000 +Subject: [PATCH 34/56] syscall: use openat2(RESOLVE_BENEATH) on Linux for + secure_relative_open + +The CVE fix in commit c35e283 made secure_relative_open() walk every +component of relpath with O_NOFOLLOW. That blocks every symlink in the +path, which is stricter than the threat model required: legitimate +directory symlinks within the destination tree (e.g. when using -K / +--copy-dirlinks) are also rejected, breaking delta transfers with +"failed verification -- update discarded". See issue #715. + +On Linux 5.6+, openat2(RESOLVE_BENEATH | RESOLVE_NO_MAGICLINKS) gives +us exactly what we want: the kernel rejects any resolution that would +escape the starting directory (via "..", absolute paths, or symlinks +pointing outside dirfd) while still following symlinks that resolve +within it. /proc magic-links are blocked too. + +Use openat2 first; fall back to the existing per-component O_NOFOLLOW +walk on ENOSYS (kernel < 5.6). The lexical "../" checks at the head +of the function are kept as defense in depth. The Linux gate is +plain #ifdef __linux__: the runtime ENOSYS fallback covers the only +case that actually matters (header present + old kernel), and any +Linux build environment without linux/openat2.h will fail with a +clear "no such file" error rather than silently disabling the +protection. + +Verified manually that openat2(RESOLVE_BENEATH) blocks all four +escape patterns (absolute symlink, ../ symlink, lexical .., absolute +path) while allowing direct and within-tree symlinks. The new +testsuite/symlink-dirlink-basis.test (taken from PR #864 by Samuel +Henrique) exercises the issue #715 regression and passes; full +make check passes 47/47. + +Test: testsuite/symlink-dirlink-basis.test (8 scenarios) +Fixes: https://github.com/RsyncProject/rsync/issues/715 + +Co-Authored-By: Claude Opus 4.7 (1M context) +--- + syscall.c | 64 ++++++- + testsuite/symlink-dirlink-basis.test | 247 +++++++++++++++++++++++++++ + 2 files changed, 305 insertions(+), 6 deletions(-) + create mode 100755 testsuite/symlink-dirlink-basis.test + +diff --git a/syscall.c b/syscall.c +index 21f9382e..0881c7ab 100644 +--- a/syscall.c ++++ b/syscall.c +@@ -33,6 +33,11 @@ + #include + #endif + ++#ifdef __linux__ ++#include ++#include ++#endif ++ + #include "ifuncs.h" + + extern int dry_run; +@@ -715,12 +720,49 @@ int do_open_nofollow(const char *pathname, int flags) + /* + open a file relative to a base directory. The basedir can be NULL, + in which case the current working directory is used. The relpath +- must be a relative path, and the relpath must not contain any +- elements in the path which follow symlinks (ie. like O_NOFOLLOW, but +- applies to all path components, not just the last component) +- +- The relpath must also not contain any ../ elements in the path ++ must be a relative path. The kernel must guarantee that resolution ++ cannot escape basedir (or the cwd, when basedir is NULL): no ".." ++ jumps above the start, no symlinks pointing outside, no absolute ++ paths, no /proc magic-link tricks. ++ ++ Symlinks *within* basedir are followed normally — earlier rsync ++ versions rejected every symlink with O_NOFOLLOW on each component, ++ which broke legitimate directory symlinks on the receiver side ++ (https://github.com/RsyncProject/rsync/issues/715). The escape ++ prevention is handled by the kernel via openat2(RESOLVE_BENEATH) ++ on Linux 5.6+; older systems fall back to the per-component ++ O_NOFOLLOW walk below. ++ ++ The relpath must also not contain any ../ elements in the path. + */ ++ ++#ifdef __linux__ ++static int secure_relative_open_linux(const char *basedir, const char *relpath, int flags, mode_t mode) ++{ ++ struct open_how how; ++ int dirfd, retfd; ++ ++ memset(&how, 0, sizeof how); ++ how.flags = flags; ++ how.mode = mode; ++ how.resolve = RESOLVE_BENEATH | RESOLVE_NO_MAGICLINKS; ++ ++ if (basedir == NULL) { ++ dirfd = AT_FDCWD; ++ } else { ++ dirfd = openat(AT_FDCWD, basedir, O_RDONLY | O_DIRECTORY); ++ if (dirfd == -1) ++ return -1; ++ } ++ ++ retfd = syscall(SYS_openat2, dirfd, relpath, &how, sizeof how); ++ ++ if (dirfd != AT_FDCWD) ++ close(dirfd); ++ return retfd; ++} ++#endif ++ + int secure_relative_open(const char *basedir, const char *relpath, int flags, mode_t mode) + { + if (!relpath || relpath[0] == '/') { +@@ -734,7 +776,17 @@ int secure_relative_open(const char *basedir, const char *relpath, int flags, mo + return -1; + } + +-#if !defined(O_NOFOLLOW) || !defined(O_DIRECTORY) ++#ifdef __linux__ ++ { ++ int fd = secure_relative_open_linux(basedir, relpath, flags, mode); ++ /* ENOSYS = kernel < 5.6 doesn't have the syscall even though ++ * glibc/kernel-headers do; fall through to the portable path. */ ++ if (fd != -1 || errno != ENOSYS) ++ return fd; ++ } ++#endif ++ ++#if !defined(O_NOFOLLOW) || !defined(O_DIRECTORY) || !defined(AT_FDCWD) + // really old system, all we can do is live with the risks + if (!basedir) { + return open(relpath, flags, mode); +diff --git a/testsuite/symlink-dirlink-basis.test b/testsuite/symlink-dirlink-basis.test +new file mode 100755 +index 00000000..9065dd81 +--- /dev/null ++++ b/testsuite/symlink-dirlink-basis.test +@@ -0,0 +1,247 @@ ++#!/bin/sh ++ ++# Test that updating a file through a directory symlink works when using ++# -K (--copy-dirlinks). This is a regression test for: ++# https://github.com/RsyncProject/rsync/issues/715 ++# ++# The CVE fix in commit c35e283 introduced secure_relative_open() which ++# uses O_NOFOLLOW on all path components, breaking legitimate directory ++# symlinks on the receiver side. The fix splits the path into basedir ++# (dirname, symlinks followed) and basename (O_NOFOLLOW) so that ++# directory symlinks are traversed while the final file component is ++# still protected. ++# ++# The regression only manifests when delta matching is triggered (i.e., ++# the sender finds matching blocks in the old file). Small files with ++# completely different content are transferred in full and don't trigger ++# the bug. We use a large file with a small modification to ensure ++# delta transfer is used. ++# ++# In addition to the original regression, this test covers edge cases ++# in the fix itself: ++# - --backup with directory symlinks (finish_transfer pointer identity) ++# - --partial-dir with protocol < 29 (fnamecmp != partialptr guard) ++# - --inplace with directory symlinks (updating_basis_or_equiv check) ++# - Files without a dirname (top-level files, no split needed) ++ ++. "$suitedir/rsync.fns" ++ ++RSYNC_RSH="$scratchdir/src/support/lsh.sh" ++export RSYNC_RSH ++ ++# $HOME is set to $scratchdir by rsync.fns ++# localhost: destination will cd to $HOME (i.e., $scratchdir) ++ ++# Helper: create a large file suitable for delta transfers. ++# ~32KB is large enough for rsync's block matching to find matches. ++make_testfile() { ++ dd if=/dev/urandom of="$1" bs=1024 count=32 2>/dev/null \ ++ || test_fail "failed to create test file $1" ++} ++ ++# Set up source tree ++srcbase="$tmpdir/src" ++ ++###################################################################### ++# Test 1: Basic directory symlink update (the original issue #715) ++###################################################################### ++ ++mkdir -p "$HOME/real-dir" ++ln -s real-dir "$HOME/dir" ++ ++mkdir -p "$srcbase/dir" ++make_testfile "$srcbase/dir/file" ++ ++# First transfer (initial): should create the file through the symlink ++(cd "$srcbase" && $RSYNC -KRlptv --rsync-path="$RSYNC" dir/file localhost:) \ ++ || test_fail "test 1: initial transfer failed" ++ ++if [ ! -f "$HOME/real-dir/file" ]; then ++ test_fail "test 1: initial transfer did not create file through symlink" ++fi ++ ++diff "$srcbase/dir/file" "$HOME/real-dir/file" >/dev/null \ ++ || test_fail "test 1: initial transfer content mismatch" ++ ++# Small modification to trigger delta transfer ++echo "appended update" >> "$srcbase/dir/file" ++sleep 1 ++touch "$srcbase/dir/file" ++ ++# Second transfer (update): was failing with "failed verification" ++(cd "$srcbase" && $RSYNC -KRlptv --rsync-path="$RSYNC" dir/file localhost:) \ ++ || test_fail "test 1: update through directory symlink failed" ++ ++diff "$srcbase/dir/file" "$HOME/real-dir/file" >/dev/null \ ++ || test_fail "test 1: update transfer content mismatch" ++ ++###################################################################### ++# Test 2: Compression (-z) as in the original reproducer ++###################################################################### ++ ++echo "another line" >> "$srcbase/dir/file" ++sleep 1 ++touch "$srcbase/dir/file" ++ ++(cd "$srcbase" && $RSYNC -KRlptzv --rsync-path="$RSYNC" dir/file localhost:) \ ++ || test_fail "test 2: compressed update through directory symlink failed" ++ ++diff "$srcbase/dir/file" "$HOME/real-dir/file" >/dev/null \ ++ || test_fail "test 2: compressed update content mismatch" ++ ++###################################################################### ++# Test 3: Nested directory symlinks (nested/sub/data.txt where ++# "nested" is a symlink to "nested_real") ++###################################################################### ++ ++mkdir -p "$HOME/nested_real/sub" ++ln -s nested_real "$HOME/nested" ++ ++mkdir -p "$srcbase/nested/sub" ++make_testfile "$srcbase/nested/sub/data.txt" ++ ++(cd "$srcbase" && $RSYNC -KRlptv --rsync-path="$RSYNC" nested/sub/data.txt localhost:) \ ++ || test_fail "test 3: initial nested transfer failed" ++ ++echo "appended nested" >> "$srcbase/nested/sub/data.txt" ++sleep 1 ++touch "$srcbase/nested/sub/data.txt" ++ ++(cd "$srcbase" && $RSYNC -KRlptv --rsync-path="$RSYNC" nested/sub/data.txt localhost:) \ ++ || test_fail "test 3: update through nested directory symlink failed" ++ ++diff "$srcbase/nested/sub/data.txt" "$HOME/nested_real/sub/data.txt" >/dev/null \ ++ || test_fail "test 3: nested update content mismatch" ++ ++###################################################################### ++# Test 4: --backup with directory symlinks ++# ++# Exercises the finish_transfer() "fnamecmp == fname" pointer ++# comparison that determines whether to update fnamecmp to the ++# backup name. If broken, --backup would reference a renamed file ++# for xattr handling. ++###################################################################### ++ ++# Reset destination ++rm -f "$HOME/real-dir/file" "$HOME/real-dir/file~" ++ ++make_testfile "$srcbase/dir/file" ++ ++(cd "$srcbase" && $RSYNC -KRlptv --rsync-path="$RSYNC" dir/file localhost:) \ ++ || test_fail "test 4: initial transfer for backup test failed" ++ ++echo "backup update" >> "$srcbase/dir/file" ++sleep 1 ++touch "$srcbase/dir/file" ++ ++(cd "$srcbase" && $RSYNC -KRlptv --backup --rsync-path="$RSYNC" dir/file localhost:) \ ++ || test_fail "test 4: update with --backup through directory symlink failed" ++ ++diff "$srcbase/dir/file" "$HOME/real-dir/file" >/dev/null \ ++ || test_fail "test 4: backup update content mismatch" ++ ++if [ ! -f "$HOME/real-dir/file~" ]; then ++ test_fail "test 4: backup file was not created" ++fi ++ ++###################################################################### ++# Test 5: --inplace with directory symlinks ++# ++# Exercises the updating_basis_or_equiv check which uses ++# "fnamecmp == fname". With --inplace, rsync writes directly to ++# the destination file instead of a temp file. ++###################################################################### ++ ++rm -f "$HOME/real-dir/file" "$HOME/real-dir/file~" ++ ++make_testfile "$srcbase/dir/file" ++ ++(cd "$srcbase" && $RSYNC -KRlptv --inplace --rsync-path="$RSYNC" dir/file localhost:) \ ++ || test_fail "test 5: initial inplace transfer failed" ++ ++echo "inplace update" >> "$srcbase/dir/file" ++sleep 1 ++touch "$srcbase/dir/file" ++ ++(cd "$srcbase" && $RSYNC -KRlptv --inplace --rsync-path="$RSYNC" dir/file localhost:) \ ++ || test_fail "test 5: inplace update through directory symlink failed" ++ ++diff "$srcbase/dir/file" "$HOME/real-dir/file" >/dev/null \ ++ || test_fail "test 5: inplace update content mismatch" ++ ++###################################################################### ++# Test 6: Top-level file (no dirname, no split needed) ++# ++# Ensures the dirname/basename split is not attempted for files ++# at the top level (file->dirname is NULL). ++###################################################################### ++ ++make_testfile "$srcbase/topfile" ++mkdir -p "$HOME" ++ ++(cd "$srcbase" && $RSYNC -Rlptv --rsync-path="$RSYNC" topfile localhost:) \ ++ || test_fail "test 6: initial top-level transfer failed" ++ ++echo "toplevel update" >> "$srcbase/topfile" ++sleep 1 ++touch "$srcbase/topfile" ++ ++(cd "$srcbase" && $RSYNC -Rlptv --rsync-path="$RSYNC" topfile localhost:) \ ++ || test_fail "test 6: top-level update failed" ++ ++diff "$srcbase/topfile" "$HOME/topfile" >/dev/null \ ++ || test_fail "test 6: top-level update content mismatch" ++ ++###################################################################### ++# Test 7: --partial-dir with protocol < 29 ++# ++# For protocol < 29, fnamecmp_type stays FNAMECMP_FNAME even when ++# fnamecmp is set to partialptr. The dirname/basename split must ++# NOT trigger in this case (guarded by "fnamecmp == fname"). ++###################################################################### ++ ++rm -f "$HOME/real-dir/file" ++make_testfile "$srcbase/dir/file" ++ ++(cd "$srcbase" && $RSYNC -KRlptv --protocol=28 --partial-dir=.rsync-partial \ ++ --rsync-path="$RSYNC" dir/file localhost:) \ ++ || test_fail "test 7: initial proto28 partial-dir transfer failed" ++ ++echo "partial-dir update" >> "$srcbase/dir/file" ++sleep 1 ++touch "$srcbase/dir/file" ++ ++(cd "$srcbase" && $RSYNC -KRlptv --protocol=28 --partial-dir=.rsync-partial \ ++ --rsync-path="$RSYNC" dir/file localhost:) \ ++ || test_fail "test 7: proto28 partial-dir update through dirlink failed" ++ ++diff "$srcbase/dir/file" "$HOME/real-dir/file" >/dev/null \ ++ || test_fail "test 7: proto28 partial-dir update content mismatch" ++ ++###################################################################### ++# Test 8: Protocol < 29 basic directory symlink update ++# ++# Exercises the protocol < 29 code path and its fallback logic ++# (clearing basedir on retry). ++###################################################################### ++ ++rm -f "$HOME/real-dir/file" ++make_testfile "$srcbase/dir/file" ++ ++(cd "$srcbase" && $RSYNC -KRlptv --protocol=28 \ ++ --rsync-path="$RSYNC" dir/file localhost:) \ ++ || test_fail "test 8: initial proto28 transfer failed" ++ ++echo "proto28 update" >> "$srcbase/dir/file" ++sleep 1 ++touch "$srcbase/dir/file" ++ ++(cd "$srcbase" && $RSYNC -KRlptv --protocol=28 \ ++ --rsync-path="$RSYNC" dir/file localhost:) \ ++ || test_fail "test 8: proto28 update through directory symlink failed" ++ ++diff "$srcbase/dir/file" "$HOME/real-dir/file" >/dev/null \ ++ || test_fail "test 8: proto28 update content mismatch" ++ ++# The script would have aborted on error, so getting here means we've won. ++exit 0 +-- +2.51.0 + diff -Nru rsync-3.2.7/debian/patches/2026-05-20/0035-syscall-also-use-O_RESOLVE_BENEATH-on-FreeBSD-and-Ma.patch rsync-3.2.7/debian/patches/2026-05-20/0035-syscall-also-use-O_RESOLVE_BENEATH-on-FreeBSD-and-Ma.patch --- rsync-3.2.7/debian/patches/2026-05-20/0035-syscall-also-use-O_RESOLVE_BENEATH-on-FreeBSD-and-Ma.patch 1970-01-01 00:00:00.000000000 +0000 +++ rsync-3.2.7/debian/patches/2026-05-20/0035-syscall-also-use-O_RESOLVE_BENEATH-on-FreeBSD-and-Ma.patch 2026-05-20 05:47:50.000000000 +0000 @@ -0,0 +1,96 @@ +From 61d987c54a472d88855c5fbef3a4c7b51696f93a Mon Sep 17 00:00:00 2001 +From: Andrew Tridgell +Date: Thu, 30 Apr 2026 08:44:11 +1000 +Subject: [PATCH 35/56] syscall: also use O_RESOLVE_BENEATH on FreeBSD and + MacOS + +FreeBSD and MacOS have O_RESOLVE_BENEATH as an openat() flag with the same +"must not escape dirfd" semantics as Linux's RESOLVE_BENEATH. The +kernel rejects ".." escapes, absolute symlinks, and symlinks whose +target lies outside dirfd, while still following symlinks that +resolve within it -- the same trade-off that fixes issue #715 on +Linux. + +Add a parallel BSD path in secure_relative_open(), gated on +declared. Unlike Linux, BSD doesn't have the header/runtime split +where the symbol can exist without kernel support, so no runtime +fallback is needed: if the flag compiles in, the kernel honours it. + +OpenBSD and NetBSD have no equivalent kernel primitive and continue +to use the existing per-component O_NOFOLLOW walk; issue #715 +remains visible on those platforms (a userland resolver or +unveil(2)-based fence would be follow-up work). + +Co-Authored-By: Claude Opus 4.7 (1M context) +--- + syscall.c | 40 +++++++++++++++++++++++++++++++++++++--- + 1 file changed, 37 insertions(+), 3 deletions(-) + +diff --git a/syscall.c b/syscall.c +index 0881c7ab..045c6ce3 100644 +--- a/syscall.c ++++ b/syscall.c +@@ -729,9 +729,13 @@ int do_open_nofollow(const char *pathname, int flags) + versions rejected every symlink with O_NOFOLLOW on each component, + which broke legitimate directory symlinks on the receiver side + (https://github.com/RsyncProject/rsync/issues/715). The escape +- prevention is handled by the kernel via openat2(RESOLVE_BENEATH) +- on Linux 5.6+; older systems fall back to the per-component +- O_NOFOLLOW walk below. ++ prevention is handled by: ++ Linux 5.6+: openat2(RESOLVE_BENEATH) ++ FreeBSD 13+: openat() with O_RESOLVE_BENEATH ++ macOS 15+ / iOS 18+: openat() with O_RESOLVE_BENEATH (same ++ flag name, picked up by the same #ifdef; ++ flag value differs from FreeBSD) ++ Other systems fall back to the per-component O_NOFOLLOW walk below. + + The relpath must also not contain any ../ elements in the path. + */ +@@ -763,6 +767,32 @@ static int secure_relative_open_linux(const char *basedir, const char *relpath, + } + #endif + ++#ifdef O_RESOLVE_BENEATH ++/* FreeBSD 13+ and macOS 15+ (Sequoia) / iOS 18+: O_RESOLVE_BENEATH is ++ * an openat() flag with the same "must not escape dirfd" semantics as ++ * Linux's RESOLVE_BENEATH. The kernel rejects ".." escapes, absolute ++ * symlinks, and symlinks whose target lies outside dirfd. (FreeBSD and ++ * Apple use different flag bit values, but the same symbolic name.) */ ++static int secure_relative_open_resolve_beneath(const char *basedir, const char *relpath, int flags, mode_t mode) ++{ ++ int dirfd, retfd; ++ ++ if (basedir == NULL) { ++ dirfd = AT_FDCWD; ++ } else { ++ dirfd = openat(AT_FDCWD, basedir, O_RDONLY | O_DIRECTORY); ++ if (dirfd == -1) ++ return -1; ++ } ++ ++ retfd = openat(dirfd, relpath, flags | O_RESOLVE_BENEATH, mode); ++ ++ if (dirfd != AT_FDCWD) ++ close(dirfd); ++ return retfd; ++} ++#endif ++ + int secure_relative_open(const char *basedir, const char *relpath, int flags, mode_t mode) + { + if (!relpath || relpath[0] == '/') { +@@ -786,6 +816,10 @@ int secure_relative_open(const char *basedir, const char *relpath, int flags, mo + } + #endif + ++#ifdef O_RESOLVE_BENEATH ++ return secure_relative_open_resolve_beneath(basedir, relpath, flags, mode); ++#endif ++ + #if !defined(O_NOFOLLOW) || !defined(O_DIRECTORY) || !defined(AT_FDCWD) + // really old system, all we can do is live with the risks + if (!basedir) { +-- +2.51.0 + diff -Nru rsync-3.2.7/debian/patches/2026-05-20/0036-testsuite-skip-symlink-dirlink-basis-on-platforms-wi.patch rsync-3.2.7/debian/patches/2026-05-20/0036-testsuite-skip-symlink-dirlink-basis-on-platforms-wi.patch --- rsync-3.2.7/debian/patches/2026-05-20/0036-testsuite-skip-symlink-dirlink-basis-on-platforms-wi.patch 1970-01-01 00:00:00.000000000 +0000 +++ rsync-3.2.7/debian/patches/2026-05-20/0036-testsuite-skip-symlink-dirlink-basis-on-platforms-wi.patch 2026-05-20 05:47:50.000000000 +0000 @@ -0,0 +1,49 @@ +From 2b3f8aacc7eca828574a304a0f2b88fbcdaa04e3 Mon Sep 17 00:00:00 2001 +From: Andrew Tridgell +Date: Thu, 30 Apr 2026 09:00:09 +1000 +Subject: [PATCH 36/56] testsuite: skip symlink-dirlink-basis on platforms + without RESOLVE_BENEATH + +secure_relative_open() has a kernel-enforced "stay below dirfd" path +on Linux 5.6+ (openat2 RESOLVE_BENEATH) and FreeBSD 13+ (openat +O_RESOLVE_BENEATH). On Solaris, OpenBSD, NetBSD, and Cygwin the code +falls back to the per-component O_NOFOLLOW walk, which by design +rejects every directory symlink in the path -- the very case this +test exercises. Mark the test skipped there rather than have it +fail with a known regression that's tracked separately. + +macOS is intentionally not in the skip list: although it does not +have O_RESOLVE_BENEATH either, the test passes there in practice; +investigation of the underlying reason is left as follow-up. + +Co-Authored-By: Claude Opus 4.7 (1M context) +--- + testsuite/symlink-dirlink-basis.test | 12 ++++++++++++ + 1 file changed, 12 insertions(+) + +diff --git a/testsuite/symlink-dirlink-basis.test b/testsuite/symlink-dirlink-basis.test +index 9065dd81..a14eb5cf 100755 +--- a/testsuite/symlink-dirlink-basis.test ++++ b/testsuite/symlink-dirlink-basis.test +@@ -26,6 +26,18 @@ + + . "$suitedir/rsync.fns" + ++# secure_relative_open() uses kernel-enforced "stay below dirfd" via ++# openat2(RESOLVE_BENEATH) on Linux 5.6+ and openat(O_RESOLVE_BENEATH) ++# on FreeBSD 13+. Other platforms fall back to a per-component ++# O_NOFOLLOW walk that rejects every symlink including legitimate ++# directory symlinks -- the very case this test exercises. Skip on ++# those rather than report a known failure. ++case "$(uname -s)" in ++ SunOS|OpenBSD|NetBSD|CYGWIN*) ++ test_skipped "secure_relative_open lacks RESOLVE_BENEATH equivalent on $(uname -s); issue #715 still affects this platform" ++ ;; ++esac ++ + RSYNC_RSH="$scratchdir/src/support/lsh.sh" + export RSYNC_RSH + +-- +2.51.0 + diff -Nru rsync-3.2.7/debian/patches/2026-05-20/0037-syscall-clientserver-am_chrooted-and-use_secure_syml.patch rsync-3.2.7/debian/patches/2026-05-20/0037-syscall-clientserver-am_chrooted-and-use_secure_syml.patch --- rsync-3.2.7/debian/patches/2026-05-20/0037-syscall-clientserver-am_chrooted-and-use_secure_syml.patch 1970-01-01 00:00:00.000000000 +0000 +++ rsync-3.2.7/debian/patches/2026-05-20/0037-syscall-clientserver-am_chrooted-and-use_secure_syml.patch 2026-05-20 05:47:50.000000000 +0000 @@ -0,0 +1,325 @@ +From 1a5ad81add1004354a3d8ba841b94ffe19cd2505 Mon Sep 17 00:00:00 2001 +From: Andrew Tridgell +Date: Wed, 31 Dec 2025 10:01:23 +1100 +Subject: [PATCH 37/56] syscall+clientserver: am_chrooted and + use_secure_symlinks for daemon-no-chroot (CVE-2026-29518) + +CVE-2026-29518: an rsync daemon configured with "use chroot = no" +is exposed to a TOCTOU race on parent path components. A local +attacker with write access to a module can replace a parent +directory component with a symlink between the receiver's check +and its open(), redirecting reads (basis-file disclosure) and +writes (file overwrite) outside the module. Under elevated daemon +privilege this allows privilege escalation. Default +"use chroot = yes" is not exposed. + +Add secure_relative_open() in syscall.c. It walks the parent +components under RESOLVE_BENEATH (Linux 5.6+) / +O_RESOLVE_BENEATH (FreeBSD 13+, macOS 15+) / per-component +O_NOFOLLOW elsewhere, anchored at a trusted dirfd, so a parent- +symlink swap is rejected by the kernel. Route the receiver's +basis-file open in receiver.c through it when use_secure_symlinks +is set in clientserver.c rsync_module(). + +Reporters: Nullx3D (Batuhan SANCAK); Damien Neil; Michael Stapelberg. + +Co-Authored-By: Claude Opus 4.7 (1M context) +--- + clientserver.c | 25 +++++++++ + options.c | 9 ++++ + receiver.c | 22 ++++++-- + syscall.c | 139 +++++++++++++++++++++++++++++++++++++++++++++++++ + 4 files changed, 192 insertions(+), 3 deletions(-) + +diff --git a/clientserver.c b/clientserver.c +index 3800f0d6..e8dfddb1 100644 +--- a/clientserver.c ++++ b/clientserver.c +@@ -30,6 +30,7 @@ extern int list_only; + extern int am_sender; + extern int am_server; + extern int am_daemon; ++extern int am_chrooted; + extern int am_root; + extern int msgs2stderr; + extern int rsync_port; +@@ -38,6 +39,7 @@ extern int ignore_errors; + extern int preserve_xattrs; + extern int kluge_around_eof; + extern int munge_symlinks; ++extern int use_secure_symlinks; + extern int open_noatime; + extern int sanitize_paths; + extern int numeric_ids; +@@ -983,6 +985,7 @@ static int rsync_module(int f_in, int f_out, int i, const char *addr, const char + io_printf(f_out, "@ERROR: chroot failed\n"); + return -1; + } ++ am_chrooted = 1; + module_chdir = module_dir; + } + +@@ -1005,6 +1008,15 @@ static int rsync_module(int f_in, int f_out, int i, const char *addr, const char + } + } + ++ /* Enable secure symlink handling for any non-chrooted daemon module. ++ * This prevents TOCTOU race attacks where an attacker could switch a ++ * directory to a symlink between path validation and file open. ++ * Match the gate used by the do_*_at() wrappers in syscall.c ++ * (am_daemon && !am_chrooted) -- the protection has nothing to do ++ * with symlink munging, so a module configured with ++ * "munge symlinks = false" must still get the secure-open path. */ ++ use_secure_symlinks = am_daemon && !am_chrooted; ++ + if (gid_list.count) { + gid_t *gid_array = gid_list.items; + if (setgid(gid_array[0])) { +@@ -1308,6 +1320,19 @@ int start_daemon(int f_in, int f_out) + rsyserr(FLOG, errno, "daemon chroot(\"%s\") failed", p); + return -1; + } ++ /* Deliberately do NOT set am_chrooted here. am_chrooted ++ * gates the per-module symlink-race defenses ++ * (secure_relative_open() and the do_*_at() wrappers in ++ * syscall.c) and means "the kernel is enforcing path ++ * confinement at the module boundary". The daemon chroot ++ * confines path resolution to the daemon-chroot directory, ++ * not to any individual module path -- modules sharing the ++ * daemon chroot are still distinguishable filesystem ++ * subtrees and a sender-controlled symlink in module A ++ * could redirect a syscall to module B (or to other files ++ * inside the daemon chroot) without the per-module ++ * defenses. Leave am_chrooted=0 here so secure_relative_open() ++ * still fires for "use chroot = no" modules. */ + if (chdir("/") < 0) { + rsyserr(FLOG, errno, "daemon chdir(\"/\") failed"); + return -1; +diff --git a/options.c b/options.c +index f01b25ee..73e88f70 100644 +--- a/options.c ++++ b/options.c +@@ -113,11 +113,20 @@ int mkpath_dest_arg = 0; + int allow_inc_recurse = 1; + int xfer_dirs = -1; + int am_daemon = 0; ++/* Set after a successful per-module chroot ("use chroot = yes") in ++ * clientserver.c. NOT set for the daemon-level "daemon chroot = /X" ++ * chroot: that confines path resolution to /X, but module paths ++ * /X/modA, /X/modB, etc. are not chroot boundaries, so the per-module ++ * symlink-race defenses (secure_relative_open() / do_*_at() in ++ * syscall.c, gated by `am_daemon && !am_chrooted`) must still fire ++ * even when the daemon is inside a daemon chroot. */ ++int am_chrooted = 0; + int connect_timeout = 0; + int keep_partial = 0; + int safe_symlinks = 0; + int copy_unsafe_links = 0; + int munge_symlinks = 0; ++int use_secure_symlinks = 0; + int size_only = 0; + int daemon_bwlimit = 0; + int bwlimit = 0; +diff --git a/receiver.c b/receiver.c +index 77de8697..cbe18196 100644 +--- a/receiver.c ++++ b/receiver.c +@@ -70,6 +70,7 @@ extern int fuzzy_basis; + + extern struct name_num_item *xfer_sum_nni; + extern int xfer_sum_len; ++extern int use_secure_symlinks; + + static struct bitbag *delayed_bits = NULL; + static int phase = 0, redoing = 0; +@@ -214,7 +215,12 @@ int open_tmpfile(char *fnametmp, const char *fname, struct file_struct *file) + * access to ensure that there is no race condition. They will be + * correctly updated after the right owner and group info is set. + * (Thanks to snabb@epipe.fi for pointing this out.) */ +- fd = do_mkstemp(fnametmp, (file->mode|added_perms) & INITACCESSPERMS); ++ /* When use_secure_symlinks is on (non-chroot daemon with munge_symlinks), ++ * use secure_mkstemp to prevent symlink race attacks on parent directories. */ ++ if (use_secure_symlinks) ++ fd = secure_mkstemp(fnametmp, (file->mode|added_perms) & INITACCESSPERMS); ++ else ++ fd = do_mkstemp(fnametmp, (file->mode|added_perms) & INITACCESSPERMS); + + #if 0 + /* In most cases parent directories will already exist because their +@@ -854,11 +860,21 @@ int recv_files(int f_in, int f_out, char *local_name) + /* We now check to see if we are writing the file "inplace" */ + if (inplace || one_inplace) { + fnametmp = one_inplace ? partialptr : fname; +- fd2 = do_open(fnametmp, O_WRONLY|O_CREAT, 0600); ++ /* When use_secure_symlinks is on (non-chroot daemon), ++ * use secure open to prevent symlink race attacks where an ++ * attacker could switch a directory to a symlink between ++ * path validation and file open. */ ++ if (use_secure_symlinks) ++ fd2 = secure_relative_open(NULL, fnametmp, O_WRONLY|O_CREAT, 0600); ++ else ++ fd2 = do_open(fnametmp, O_WRONLY|O_CREAT, 0600); + #ifdef linux + if (fd2 == -1 && errno == EACCES) { + /* Maybe the error was due to protected_regular setting? */ +- fd2 = do_open(fname, O_WRONLY, 0600); ++ if (use_secure_symlinks) ++ fd2 = secure_relative_open(NULL, fname, O_WRONLY, 0600); ++ else ++ fd2 = do_open(fname, O_WRONLY, 0600); + } + #endif + if (fd2 == -1) { +diff --git a/syscall.c b/syscall.c +index 045c6ce3..bfaaaa63 100644 +--- a/syscall.c ++++ b/syscall.c +@@ -877,6 +877,145 @@ cleanup: + #endif // O_NOFOLLOW, O_DIRECTORY + } + ++/* Fill buf with len random bytes. Prefers /dev/urandom for cryptographic ++ * quality; falls back to rand() if /dev/urandom cannot be opened or read ++ * (e.g. inside a chroot or container without /dev populated). */ ++static void rand_bytes(unsigned char *buf, size_t len) ++{ ++#ifndef O_CLOEXEC ++#define O_CLOEXEC 0 ++#endif ++ int fd = open("/dev/urandom", O_RDONLY | O_CLOEXEC); ++ if (fd >= 0) { ++ ssize_t n = read(fd, buf, len); ++ close(fd); ++ if (n == (ssize_t)len) { ++ return; ++ } ++ } ++ for (size_t i = 0; i < len; i++) { ++ buf[i] = (unsigned char)rand(); ++ } ++} ++ ++/* ++ Secure version of mkstemp that prevents symlink attacks on parent directories. ++ Like secure_relative_open(), this walks the path checking each component ++ with O_NOFOLLOW to prevent TOCTOU race conditions. ++ ++ The template may be relative or absolute, but must not contain ../ components. ++ Returns fd on success, -1 on error. ++*/ ++int secure_mkstemp(char *template, mode_t perms) ++{ ++#if !defined(O_NOFOLLOW) || !defined(O_DIRECTORY) || !defined(AT_FDCWD) ++ /* Fall back to regular mkstemp on old systems */ ++ return do_mkstemp(template, perms); ++#else ++ char *lastslash; ++ int dirfd = AT_FDCWD; ++ int fd = -1; ++ ++ if (!template) { ++ errno = EINVAL; ++ return -1; ++ } ++ if (strncmp(template, "../", 3) == 0 || strstr(template, "/../")) { ++ errno = EINVAL; ++ return -1; ++ } ++ ++ /* For absolute paths, start the secure walk from "/" rather than CWD. */ ++ if (template[0] == '/') { ++ dirfd = open("/", O_RDONLY | O_DIRECTORY | O_NOFOLLOW); ++ if (dirfd < 0) ++ return -1; ++ } ++ ++ /* Find the last slash to separate directory from filename */ ++ lastslash = strrchr(template, '/'); ++ if (lastslash) { ++ char *path_copy = my_strdup(template, __FILE__, __LINE__); ++ if (!path_copy) ++ return -1; ++ ++ /* Null-terminate at the last slash to get directory part */ ++ path_copy[lastslash - template] = '\0'; ++ ++ /* Walk the directory path securely */ ++ for (const char *part = strtok(path_copy, "/"); ++ part != NULL; ++ part = strtok(NULL, "/")) ++ { ++ int next_fd = openat(dirfd, part, O_RDONLY | O_DIRECTORY | O_NOFOLLOW); ++ if (next_fd == -1) { ++ int save_errno = errno; ++ free(path_copy); ++ if (dirfd != AT_FDCWD) close(dirfd); ++ errno = (save_errno == ELOOP) ? ELOOP : save_errno; ++ return -1; ++ } ++ if (dirfd != AT_FDCWD) close(dirfd); ++ dirfd = next_fd; ++ } ++ free(path_copy); ++ } ++ ++ /* Now create the temp file in the securely-opened directory */ ++ perms |= S_IWUSR; ++ ++ /* Generate unique filename - we need to modify the template in place */ ++ char *filename = lastslash ? lastslash + 1 : template; ++ size_t filename_len = strlen(filename); ++ ++ if (filename_len < 6) { ++ if (dirfd != AT_FDCWD) close(dirfd); ++ errno = EINVAL; ++ return -1; ++ } ++ char *suffix = filename + filename_len - 6; /* Points to XXXXXX */ ++ if (strcmp(suffix, "XXXXXX") != 0) { ++ if (dirfd != AT_FDCWD) close(dirfd); ++ errno = EINVAL; ++ return -1; ++ } ++ ++ /* Try random suffixes until we find one that works */ ++ static const char letters[] = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; ++ for (int tries = 0; tries < 100; tries++) { ++ unsigned char rbytes[6]; ++ rand_bytes(rbytes, sizeof(rbytes)); ++ for (int i = 0; i < 6; i++) ++ suffix[i] = letters[rbytes[i] % (sizeof(letters) - 1)]; ++ ++ fd = openat(dirfd, filename, O_RDWR | O_CREAT | O_EXCL | O_NOFOLLOW, perms); ++ if (fd >= 0) ++ break; ++ if (errno != EEXIST) { ++ if (dirfd != AT_FDCWD) close(dirfd); ++ return -1; ++ } ++ } ++ ++ if (fd >= 0) { ++ if (fchmod(fd, perms) != 0 && preserve_perms) { ++ int errno_save = errno; ++ close(fd); ++ unlinkat(dirfd, filename, 0); ++ if (dirfd != AT_FDCWD) close(dirfd); ++ errno = errno_save; ++ return -1; ++ } ++#if defined HAVE_SETMODE && O_BINARY ++ setmode(fd, O_BINARY); ++#endif ++ } ++ ++ if (dirfd != AT_FDCWD) close(dirfd); ++ return fd; ++#endif ++} ++ + /* + varient of do_open/do_open_nofollow which does do_open() if the + copy_links or copy_unsafe_links options are set and does +-- +2.51.0 + diff -Nru rsync-3.2.7/debian/patches/2026-05-20/0038-sender-fix-read-path-TOCTOU-by-opening-from-module-r.patch rsync-3.2.7/debian/patches/2026-05-20/0038-sender-fix-read-path-TOCTOU-by-opening-from-module-r.patch --- rsync-3.2.7/debian/patches/2026-05-20/0038-sender-fix-read-path-TOCTOU-by-opening-from-module-r.patch 1970-01-01 00:00:00.000000000 +0000 +++ rsync-3.2.7/debian/patches/2026-05-20/0038-sender-fix-read-path-TOCTOU-by-opening-from-module-r.patch 2026-05-20 05:47:50.000000000 +0000 @@ -0,0 +1,68 @@ +From 99b36291d06ca66229942c7a525a1f5566f10c85 Mon Sep 17 00:00:00 2001 +From: Andrew Tridgell +Date: Sun, 1 Mar 2026 09:28:40 +1100 +Subject: [PATCH 38/56] sender: fix read-path TOCTOU by opening from module + root (CVE-2026-29518) + +The sender's file open was vulnerable to the same TOCTOU symlink +race as the receiver-side basis-file open. change_pathname() calls +chdir() into subdirectories, which follows symlinks; an attacker +could race to swap a directory for a symlink between the chdir and +the file open, allowing reads of privileged files through the +daemon. + +Reconstruct the full relative path (F_PATHNAME + fname) and open +via secure_relative_open() from the trusted module_dir, which +walks each path component without following symlinks. This is +independent of CWD, so the chdir race is neutralised. + +CVE-2026-29518. + +Co-Authored-By: Claude Opus 4.6 +--- + sender.c | 22 +++++++++++++++++++++- + 1 file changed, 21 insertions(+), 1 deletion(-) + +diff --git a/sender.c b/sender.c +index b1588b70..99f431fe 100644 +--- a/sender.c ++++ b/sender.c +@@ -48,6 +48,8 @@ extern int make_backups; + extern int inplace; + extern int inplace_partial; + extern int batch_fd; ++extern int use_secure_symlinks; ++extern char *module_dir; + extern int write_batch; + extern int file_old_total; + extern BOOL want_progress_now; +@@ -352,7 +354,25 @@ void send_files(int f_in, int f_out) + exit_cleanup(RERR_PROTOCOL); + } + +- fd = do_open_checklinks(fname); ++ if (use_secure_symlinks) { ++ /* Open from module root to prevent TOCTOU race where ++ * change_pathname's chdir follows a directory symlink. ++ * Reconstruct the full path relative to module_dir ++ * from F_PATHNAME (path) and f_name (fname). */ ++ char secure_path[MAXPATHLEN]; ++ int slen = snprintf(secure_path, sizeof secure_path, "%s%s%s", path, slash, fname); ++ if (slen >= (int)sizeof secure_path) { ++ io_error |= IOERR_GENERAL; ++ rprintf(FERROR_XFER, "path too long: %s%s%s\n", path, slash, fname); ++ free_sums(s); ++ if (protocol_version >= 30) ++ send_msg_int(MSG_NO_SEND, ndx); ++ continue; ++ } ++ fd = secure_relative_open(module_dir, secure_path, O_RDONLY, 0); ++ } else { ++ fd = do_open_checklinks(fname); ++ } + if (fd == -1) { + if (errno == ENOENT) { + enum logcode c = am_daemon && protocol_version < 28 ? FERROR : FWARNING; +-- +2.51.0 + diff -Nru rsync-3.2.7/debian/patches/2026-05-20/0039-syscall-receiver-secure-receiver-side-do_chmod-again.patch rsync-3.2.7/debian/patches/2026-05-20/0039-syscall-receiver-secure-receiver-side-do_chmod-again.patch --- rsync-3.2.7/debian/patches/2026-05-20/0039-syscall-receiver-secure-receiver-side-do_chmod-again.patch 1970-01-01 00:00:00.000000000 +0000 +++ rsync-3.2.7/debian/patches/2026-05-20/0039-syscall-receiver-secure-receiver-side-do_chmod-again.patch 2026-05-20 05:47:50.000000000 +0000 @@ -0,0 +1,463 @@ +From 24852cda3db38e2f2cd78a13703373c77f75f4d5 Mon Sep 17 00:00:00 2001 +From: Andrew Tridgell +Date: Mon, 4 May 2026 21:53:14 +1000 +Subject: [PATCH 39/56] syscall+receiver: secure receiver-side do_chmod against + symlink-race TOCTOU + +CVE-2026-29518's fix routed the receiver's open() through +secure_relative_open(), but every other path-based syscall the +receiver runs on sender-controllable paths is vulnerable to the +same TOCTOU primitive. This commit closes the chmod variant. + +Add do_chmod_at() that opens the parent of fname under +secure_relative_open() and uses fchmodat() against the resulting +dirfd. Gate the secure path on am_daemon && !am_chrooted (the same +gate use_secure_symlinks already uses for the receiver basis-file +open), so non-daemon callers and chrooted daemons keep the original +do_chmod() fast path. + +Migrate the receiver-side do_chmod() call sites in delete.c, +generator.c, rsync.c, and xattrs.c. + +Adds testsuite/chmod-symlink-race.test (with t_chmod_secure helper) +as regression coverage. + +Co-Authored-By: Claude Opus 4.7 (1M context) +--- + Makefile.in | 10 ++- + delete.c | 4 +- + generator.c | 4 +- + rsync.c | 2 +- + syscall.c | 80 ++++++++++++++++++++ + t_chmod_secure.c | 117 ++++++++++++++++++++++++++++++ + t_stub.c | 2 + + testsuite/chmod-symlink-race.test | 68 +++++++++++++++++ + xattrs.c | 6 +- + 9 files changed, 282 insertions(+), 11 deletions(-) + create mode 100644 t_chmod_secure.c + create mode 100755 testsuite/chmod-symlink-race.test + +diff --git a/Makefile.in b/Makefile.in +index be3e0fc5..481f3ece 100644 +--- a/Makefile.in ++++ b/Makefile.in +@@ -57,13 +57,13 @@ TLS_OBJ = tls.o syscall.o util2.o t_stub.o lib/compat.o lib/snprintf.o lib/perms + + # Programs we must have to run the test cases + CHECK_PROGS = rsync$(EXEEXT) tls$(EXEEXT) getgroups$(EXEEXT) getfsdev$(EXEEXT) \ +- testrun$(EXEEXT) trimslash$(EXEEXT) t_unsafe$(EXEEXT) wildtest$(EXEEXT) \ +- simdtest$(EXEEXT) ++ testrun$(EXEEXT) trimslash$(EXEEXT) t_unsafe$(EXEEXT) t_chmod_secure$(EXEEXT) \ ++ wildtest$(EXEEXT) simdtest$(EXEEXT) + + CHECK_SYMLINKS = testsuite/chown-fake.test testsuite/devices-fake.test testsuite/xattrs-hlink.test + + # Objects for CHECK_PROGS to clean +-CHECK_OBJS=tls.o testrun.o getgroups.o getfsdev.o t_stub.o t_unsafe.o trimslash.o wildtest.o ++CHECK_OBJS=tls.o testrun.o getgroups.o getfsdev.o t_stub.o t_unsafe.o t_chmod_secure.o trimslash.o wildtest.o + + # note that the -I. is needed to handle config.h when using VPATH + .c.o: +@@ -179,6 +179,10 @@ T_UNSAFE_OBJ = t_unsafe.o syscall.o util1.o util2.o t_stub.o lib/compat.o lib/sn + t_unsafe$(EXEEXT): $(T_UNSAFE_OBJ) + $(CC) $(CFLAGS) $(LDFLAGS) -o $@ $(T_UNSAFE_OBJ) $(LIBS) + ++T_CHMOD_SECURE_OBJ = t_chmod_secure.o syscall.o util1.o util2.o t_stub.o lib/compat.o lib/snprintf.o lib/wildmatch.o lib/permstring.o ++t_chmod_secure$(EXEEXT): $(T_CHMOD_SECURE_OBJ) ++ $(CC) $(CFLAGS) $(LDFLAGS) -o $@ $(T_CHMOD_SECURE_OBJ) $(LIBS) ++ + .PHONY: conf + conf: configure.sh config.h.in + +diff --git a/delete.c b/delete.c +index 4a294853..3a625610 100644 +--- a/delete.c ++++ b/delete.c +@@ -98,7 +98,7 @@ static enum delret delete_dir_contents(char *fname, uint16 flags) + + strlcpy(p, fp->basename, remainder); + if (!(fp->mode & S_IWUSR) && !am_root && fp->flags & FLAG_OWNED_BY_US) +- do_chmod(fname, fp->mode | S_IWUSR); ++ do_chmod_at(fname, fp->mode | S_IWUSR); + /* Save stack by recursing to ourself directly. */ + if (S_ISDIR(fp->mode)) { + if (delete_dir_contents(fname, flags | DEL_RECURSE) != DR_SUCCESS) +@@ -139,7 +139,7 @@ enum delret delete_item(char *fbuf, uint16 mode, uint16 flags) + } + + if (flags & DEL_NO_UID_WRITE) +- do_chmod(fbuf, mode | S_IWUSR); ++ do_chmod_at(fbuf, mode | S_IWUSR); + + if (S_ISDIR(mode) && !(flags & DEL_DIR_IS_EMPTY)) { + /* This only happens on the first call to delete_item() since +diff --git a/generator.c b/generator.c +index a890a43e..c3ace1c2 100644 +--- a/generator.c ++++ b/generator.c +@@ -1499,7 +1499,7 @@ static void recv_generator(char *fname, struct file_struct *file, int ndx, + #ifdef HAVE_CHMOD + if (!am_root && (file->mode & S_IRWXU) != S_IRWXU && dir_tweaking) { + mode_t mode = file->mode | S_IRWXU; +- if (do_chmod(fname, mode) < 0) { ++ if (do_chmod_at(fname, mode) < 0) { + rsyserr(FERROR_XFER, errno, + "failed to modify permissions on %s", + full_fname(fname)); +@@ -2107,7 +2107,7 @@ static void touch_up_dirs(struct file_list *flist, int ndx) + continue; + fname = f_name(file, NULL); + if (fix_dir_perms) +- do_chmod(fname, file->mode); ++ do_chmod_at(fname, file->mode); + if (need_retouch_dir_times) { + STRUCT_STAT st; + if (link_stat(fname, &st, 0) == 0 && mtime_differs(&st, file)) { +diff --git a/rsync.c b/rsync.c +index b130aba5..cc46a2f9 100644 +--- a/rsync.c ++++ b/rsync.c +@@ -657,7 +657,7 @@ int set_file_attrs(const char *fname, struct file_struct *file, stat_x *sxp, + + #ifdef HAVE_CHMOD + if (!BITS_EQUAL(sxp->st.st_mode, new_mode, CHMOD_BITS)) { +- int ret = am_root < 0 ? 0 : do_chmod(fname, new_mode); ++ int ret = am_root < 0 ? 0 : do_chmod_at(fname, new_mode); + if (ret < 0) { + rsyserr(FERROR_XFER, errno, + "failed to set permissions on %s", +diff --git a/syscall.c b/syscall.c +index bfaaaa63..167aae0e 100644 +--- a/syscall.c ++++ b/syscall.c +@@ -281,6 +281,86 @@ int do_chmod(const char *path, mode_t mode) + return code; + return 0; + } ++ ++/* ++ Symlink-race-safe variant of do_chmod() for receiver-side use. ++ ++ Threat model: on a daemon running with "use chroot = no" (the prerequisite ++ for CVE-2026-29518), a local attacker can race a symlink swap of one of ++ the parent directory components of a path the receiver is about to chmod. ++ Because chmod() resolves symlinks at every component, the swap redirects ++ the chmod outside the receiver's confinement. ++ ++ Defence: open the *parent* directory of fname under secure_relative_open() ++ (which uses openat2(RESOLVE_BENEATH) on Linux 5.6+, openat() with ++ O_RESOLVE_BENEATH on FreeBSD 13+ and macOS 15+ (Sequoia), or a per-component ++ O_NOFOLLOW walk elsewhere) and do fchmodat() against that dirfd. A symlink ++ substituted into one of the parent components is then either followed ++ within the tree (legitimate dir-symlinks still work) or rejected by the ++ kernel (escape attempts fail). ++ ++ Final-component handling matches do_chmod(): fchmodat() with flag 0 ++ follows a symlink at the final component, which is the same behaviour as ++ chmod() and matches every current call site (the file being chmod'd is ++ one the receiver itself just created or transferred). For the rare case ++ where the caller wants to chmod a symlink-as-an-object (S_ISLNK in the ++ mode bits), we fall through to do_chmod() which has portability code for ++ that case. ++ ++ Falls back to do_chmod() for absolute paths and for paths with no parent ++ component, where there is nothing to protect against. ++*/ ++int do_chmod_at(const char *fname, mode_t mode) ++{ ++#ifdef AT_FDCWD ++ extern int am_daemon, am_chrooted; ++ char dirpath[MAXPATHLEN]; ++ const char *bname; ++ const char *slash; ++ int dfd, ret, e; ++ size_t dlen; ++ ++ if (dry_run) return 0; ++ RETURN_ERROR_IF_RO_OR_LO; ++ ++ /* Only the daemon-without-chroot case is exposed to the symlink- ++ * race attack: a chroot already confines the receiver, and a ++ * non-daemon rsync runs with the user's own authority so a ++ * symlink they planted can only redirect to files they could ++ * already access. Everywhere else, fall through to plain ++ * do_chmod() to avoid the dirfd-open overhead on every call. */ ++ if (!am_daemon || am_chrooted) ++ return do_chmod(fname, mode); ++ ++ if (!fname || !*fname || *fname == '/' || S_ISLNK(mode)) ++ return do_chmod(fname, mode); ++ ++ slash = strrchr(fname, '/'); ++ if (!slash) ++ return do_chmod(fname, mode); ++ ++ dlen = slash - fname; ++ if (dlen >= sizeof dirpath) { ++ errno = ENAMETOOLONG; ++ return -1; ++ } ++ memcpy(dirpath, fname, dlen); ++ dirpath[dlen] = '\0'; ++ bname = slash + 1; ++ ++ dfd = secure_relative_open(NULL, dirpath, O_RDONLY | O_DIRECTORY, 0); ++ if (dfd < 0) ++ return -1; ++ ++ ret = fchmodat(dfd, bname, mode, 0); ++ e = errno; ++ close(dfd); ++ errno = e; ++ return ret; ++#else ++ return do_chmod(fname, mode); ++#endif ++} + #endif + + int do_rename(const char *old_path, const char *new_path) +diff --git a/t_chmod_secure.c b/t_chmod_secure.c +new file mode 100644 +index 00000000..114dfb2d +--- /dev/null ++++ b/t_chmod_secure.c +@@ -0,0 +1,117 @@ ++/* ++ * Test harness for do_chmod_at(). Confirms the symlink-TOCTOU ++ * primitive used by CVE-2026-29518 (and its incomplete-fix follow-up ++ * for chmod) is closed by do_chmod_at(): a parent directory component ++ * being a symlink that escapes the receiver's confinement must be ++ * rejected, while a parent symlink that resolves *within* the tree ++ * must still work (so legitimate dir-symlinks are not regressed). ++ * ++ * Not linked into rsync itself. ++ * ++ * This program is free software; you can redistribute it and/or modify ++ * it under the terms of the GNU General Public License version 2 as ++ * published by the Free Software Foundation. ++ */ ++ ++#include "rsync.h" ++ ++#include ++ ++int dry_run = 0; ++int am_root = 0; ++int am_sender = 0; ++int read_only = 0; ++int list_only = 0; ++int copy_links = 0; ++int copy_unsafe_links = 0; ++extern int am_daemon, am_chrooted; ++ ++short info_levels[COUNT_INFO], debug_levels[COUNT_DEBUG]; ++ ++static int errs = 0; ++ ++static void check(const char *label, int actual_rc, int expect_ok, ++ const char *path, mode_t expected_mode) ++{ ++ struct stat st; ++ int got_ok = (actual_rc == 0); ++ if (got_ok != expect_ok) { ++ fprintf(stderr, "FAIL [%s]: rc=%d errno=%d (%s), expected %s\n", ++ label, actual_rc, errno, strerror(errno), ++ expect_ok ? "success" : "rejection"); ++ errs++; ++ return; ++ } ++ if (path && stat(path, &st) < 0) { ++ fprintf(stderr, "FAIL [%s]: stat(%s) failed: %s\n", ++ label, path, strerror(errno)); ++ errs++; ++ return; ++ } ++ if (path && (st.st_mode & 07777) != expected_mode) { ++ fprintf(stderr, ++ "FAIL [%s]: %s mode is 0%o, expected 0%o\n", ++ label, path, st.st_mode & 07777, expected_mode); ++ errs++; ++ return; ++ } ++ fprintf(stderr, "OK [%s]\n", label); ++} ++ ++int main(int argc, char **argv) ++{ ++ if (argc != 2) { ++ fprintf(stderr, "usage: %s \n", argv[0]); ++ return 2; ++ } ++ if (chdir(argv[1]) < 0) { ++ perror("chdir"); ++ return 2; ++ } ++ ++ /* Simulate the daemon-without-chroot deployment that do_chmod_at() ++ * defends. With am_daemon=0 or am_chrooted=1 the wrapper falls ++ * through to plain do_chmod() and the symlink-race test would be ++ * meaningless. */ ++ am_daemon = 1; ++ am_chrooted = 0; ++ ++ /* Test layout (all inside the directory we just chdir'd to): ++ * ++ * ./realdir/sentinel -- regular target file ++ * ./inside_link -> realdir -- legitimate dir-symlink within the tree ++ * ./escape_link -> ../trap -- attacker swap, target outside tree ++ * ../trap/sentinel -- the file the attacker wants to alter ++ * ++ * The shell wrapper that calls this helper has set both sentinel ++ * files to mode 0600 so we have a clean baseline to compare. ++ */ ++ ++ /* Scenario A: legitimate parent dir-symlink, chmod must succeed. */ ++ int rc = do_chmod_at("inside_link/sentinel", 0640); ++ check("A: legit dir-symlink within tree", ++ rc, 1, "realdir/sentinel", 0640); ++ ++ /* Scenario B: parent symlink escapes the tree -- chmod must be ++ * rejected and the outside file's mode must be unchanged. */ ++ rc = do_chmod_at("escape_link/sentinel", 0666); ++ check("B: parent symlink escapes tree (the attack)", ++ rc, 0, "../trap/sentinel", 0600); ++ ++ /* Scenario C: plain relative path with no symlink components, ++ * regression check that the safe wrapper doesn't break the ++ * normal case. */ ++ rc = do_chmod_at("realdir/sentinel", 0644); ++ check("C: plain relative path (regression check)", ++ rc, 1, "realdir/sentinel", 0644); ++ ++ /* Scenario D: top-level file, no parent directory component. ++ * Falls back to do_chmod(); should succeed. */ ++ rc = do_chmod_at("topfile", 0640); ++ check("D: top-level file, no parent component", ++ rc, 1, "topfile", 0640); ++ ++ if (errs) ++ fprintf(stderr, "%d failure(s)\n", errs); ++ return errs ? 1 : 0; ++} +diff --git a/t_stub.c b/t_stub.c +index 085378a8..904dac99 100644 +--- a/t_stub.c ++++ b/t_stub.c +@@ -23,6 +23,8 @@ + + int do_fsync = 0; + int inplace = 0; ++int am_daemon = 0; ++int am_chrooted = 0; + int modify_window = 0; + int preallocate_files = 0; + int protect_args = 0; +diff --git a/testsuite/chmod-symlink-race.test b/testsuite/chmod-symlink-race.test +new file mode 100755 +index 00000000..48bbfbb4 +--- /dev/null ++++ b/testsuite/chmod-symlink-race.test +@@ -0,0 +1,68 @@ ++#!/bin/sh ++ ++# Copyright (C) 2026 by Andrew Tridgell ++ ++# This program is distributable under the terms of the GNU GPL (see ++# COPYING). ++ ++# Regression test for the symlink-TOCTOU class of bug applied to ++# chmod() on the receiver side. The CVE-2026-29518 fix used ++# secure_relative_open() for the basis-file open, but every other ++# path-based syscall the receiver runs on sender-controllable paths ++# is vulnerable to the same primitive: a local attacker swaps a ++# symlink into one of the parent directory components between the ++# receiver's check and its act, and the syscall escapes the module. ++# ++# This test exercises the new do_chmod_at() wrapper via the ++# t_chmod_secure helper. The helper sets up two scenarios: ++# - a parent dir-symlink that resolves WITHIN the module tree ++# (legitimate -K-style use, must continue to work) ++# - a parent dir-symlink that escapes the module tree (the ++# attack, must be rejected) ++# plus two regression scenarios (plain relative path, top-level ++# file) that just confirm the safe wrapper doesn't break the ++# normal case. ++# ++# The kernel-enforced "stay below dirfd" path resolution is ++# only available on Linux 5.6+, FreeBSD 13+, and macOS 15+. ++# Skip on platforms that fall back to per-component O_NOFOLLOW ++# (Solaris, OpenBSD, NetBSD, Cygwin); the per-component fallback ++# would also reject the attack but the legitimate dir-symlink ++# scenario would fail there. ++ ++. "$suitedir/rsync.fns" ++ ++case "$(uname -s)" in ++ SunOS|OpenBSD|NetBSD|CYGWIN*) ++ test_skipped "do_chmod_at relies on RESOLVE_BENEATH-equivalent kernel support not available on $(uname -s)" ++ ;; ++esac ++ ++mod="$scratchdir/module" ++trap_outside="$scratchdir/trap" ++rm -rf "$mod" "$trap_outside" ++mkdir -p "$mod/realdir" "$trap_outside" ++ ++# Set up the four file-system objects the helper expects: ++echo bystander > "$mod/realdir/sentinel" ++chmod 0600 "$mod/realdir/sentinel" ++echo target > "$trap_outside/sentinel" ++chmod 0600 "$trap_outside/sentinel" ++ln -s realdir "$mod/inside_link" ++ln -s ../trap "$mod/escape_link" ++echo top > "$mod/topfile" ++chmod 0600 "$mod/topfile" ++ ++"$TOOLDIR/t_chmod_secure" "$mod" || \ ++ test_fail "t_chmod_secure reported failures (see stderr above)" ++ ++# Sanity-check from the shell side too: the outside file's mode must ++# still be 0600 -- the helper checked this, but a second look from ++# the shell guards against a helper-internal stat() bug. ++mode=$(stat -c '%a' "$trap_outside/sentinel" 2>/dev/null \ ++ || stat -f '%Lp' "$trap_outside/sentinel" 2>/dev/null) ++if [ "$mode" != "600" ]; then ++ test_fail "outside sentinel mode changed from 600 to $mode -- chmod escaped the module" ++fi ++ ++exit 0 +diff --git a/xattrs.c b/xattrs.c +index 65166eed..e5d0dd43 100644 +--- a/xattrs.c ++++ b/xattrs.c +@@ -1086,7 +1086,7 @@ int set_xattr(const char *fname, const struct file_struct *file, const char *fna + && !S_ISLNK(sxp->st.st_mode) + #endif + && access(fname, W_OK) < 0 +- && do_chmod(fname, (sxp->st.st_mode & CHMOD_BITS) | S_IWUSR) == 0) ++ && do_chmod_at(fname, (sxp->st.st_mode & CHMOD_BITS) | S_IWUSR) == 0) + added_write_perm = 1; + + ndx = F_XATTR(file); +@@ -1094,7 +1094,7 @@ int set_xattr(const char *fname, const struct file_struct *file, const char *fna + lst = &glst->xa_items; + int return_value = rsync_xal_set(fname, lst, fnamecmp, sxp); + if (added_write_perm) /* remove the temporary write permission */ +- do_chmod(fname, sxp->st.st_mode); ++ do_chmod_at(fname, sxp->st.st_mode); + return return_value; + } + +@@ -1211,7 +1211,7 @@ int set_stat_xattr(const char *fname, struct file_struct *file, mode_t new_mode) + mode = (fst.st_mode & _S_IFMT) | (fmode & ACCESSPERMS) + | (S_ISDIR(fst.st_mode) ? 0700 : 0600); + if (fst.st_mode != mode) +- do_chmod(fname, mode); ++ do_chmod_at(fname, mode); + if (!IS_DEVICE(fst.st_mode)) + fst.st_rdev = 0; /* just in case */ + +-- +2.51.0 + diff -Nru rsync-3.2.7/debian/patches/2026-05-20/0040-util1-secure-change_dir-against-symlink-race-chdir-e.patch rsync-3.2.7/debian/patches/2026-05-20/0040-util1-secure-change_dir-against-symlink-race-chdir-e.patch --- rsync-3.2.7/debian/patches/2026-05-20/0040-util1-secure-change_dir-against-symlink-race-chdir-e.patch 1970-01-01 00:00:00.000000000 +0000 +++ rsync-3.2.7/debian/patches/2026-05-20/0040-util1-secure-change_dir-against-symlink-race-chdir-e.patch 2026-05-20 05:47:50.000000000 +0000 @@ -0,0 +1,203 @@ +From d22b6bc7d1b1d7be9df1c0c6db1599cb7d5fd82c Mon Sep 17 00:00:00 2001 +From: Andrew Tridgell +Date: Tue, 5 May 2026 14:34:33 +1000 +Subject: [PATCH 40/56] util1: secure change_dir() against symlink-race + chdir-escape + +The receiver's chdir(2) into a destination subdirectory followed +attacker-planted symlinks at every path component. Once CWD +escaped the module, every subsequent path-relative syscall (open, +chmod, lchown, ...) inherited the escape -- defeating +secure_relative_open's RESOLVE_BENEATH anchor against AT_FDCWD, +since the anchor itself was now outside the module. + +Route change_dir's relative target through secure_relative_open() +and fchdir() to the resulting dirfd in am_daemon && !am_chrooted +mode, so the chdir step itself can no longer follow a parent- +symlink. Same treatment applied to the CD_SKIP_CHDIR / +set_path_only path so it also can't follow attacker symlinks +during path tracking. + +Adds testsuite/sender-flist-symlink-leak.test covering the +sender-side flist resolution variant of the same primitive. + +Co-Authored-By: Claude Opus 4.7 (1M context) +--- + testsuite/sender-flist-symlink-leak.test | 90 ++++++++++++++++++++++++ + util1.c | 56 +++++++++++++-- + 2 files changed, 142 insertions(+), 4 deletions(-) + create mode 100755 testsuite/sender-flist-symlink-leak.test + +diff --git a/testsuite/sender-flist-symlink-leak.test b/testsuite/sender-flist-symlink-leak.test +new file mode 100755 +index 00000000..011d93d0 +--- /dev/null ++++ b/testsuite/sender-flist-symlink-leak.test +@@ -0,0 +1,90 @@ ++#!/bin/sh ++ ++# Copyright (C) 2026 by Andrew Tridgell ++ ++# This program is distributable under the terms of the GNU GPL (see ++# COPYING). ++ ++# Regression test for codex re-check finding: the sender-side file- ++# list generator can still follow an attacker-planted symlink out of ++# the module via change_pathname() -> change_dir(...,CD_SKIP_CHDIR) ++# followed by change_dir(...,CD_NORMAL). The CD_SKIP_CHDIR sets ++# skipped_chdir=1, and the next CD_NORMAL call's secure-branch in ++# util1.c is gated on `!skipped_chdir`, so the secure path is ++# bypassed and a raw chdir(curr_dir) follows attacker-controlled ++# symlinks during flist generation. ++# ++# Reach: rsync daemon module with `use chroot = no`. A local ++# attacker plants module/cd -> /outside. A client (innocent or ++# malicious) pulls rsync:////cd/. The daemon, as ++# sender, enumerates files in /outside and ships their metadata ++# (names, sizes, modes, mtimes) to the client. The actual content ++# transfer fails later at the secure_relative_open step with EXDEV, ++# but by then the metadata has already leaked. ++# ++# We detect by running a dry-run pull of the symlinked subdir and ++# checking whether the client's --list-only output mentions any ++# file from /outside. With the bug, /outside/secret.txt appears in ++# the list with its size; with the fix, the daemon's chdir into ++# the symlinked subdir is rejected and no /outside file is listed. ++ ++. "$suitedir/rsync.fns" ++ ++case "$(uname -s)" in ++ SunOS|OpenBSD|NetBSD|CYGWIN*) ++ test_skipped "secure change_dir relies on RESOLVE_BENEATH-equivalent kernel support not available on $(uname -s)" ++ ;; ++esac ++ ++mod="$scratchdir/module" ++outside="$scratchdir/outside" ++listfile="$scratchdir/listed.txt" ++conf="$scratchdir/test-rsyncd.conf" ++ ++rm -rf "$mod" "$outside" ++mkdir -p "$mod" "$outside" ++ ++# Outside-the-module file the daemon should NOT enumerate to clients. ++# A distinctive name + non-trivial size makes the leak easy to spot. ++echo "OUTSIDE_PROTECTED_FILE_USED_AS_LEAK_DETECTOR" > "$outside/leak_marker.txt" ++chmod 0644 "$outside/leak_marker.txt" ++ ++# The symlink trap planted by the local attacker. ++ln -s "$outside" "$mod/cd" ++ ++my_uid=`get_testuid` ++root_uid=`get_rootuid` ++root_gid=`get_rootgid` ++uid_setting="uid = $root_uid" ++gid_setting="gid = $root_gid" ++if test x"$my_uid" != x"$root_uid"; then ++ uid_setting="#$uid_setting" ++ gid_setting="#$gid_setting" ++fi ++ ++cat > "$conf" < "$listfile" 2>&1 || true ++ ++if grep -q "leak_marker\.txt" "$listfile"; then ++ echo "----- leaked listing follows" >&2 ++ sed 's/^/ /' "$listfile" >&2 ++ echo "----- leaked listing ends" >&2 ++ test_fail "sender flist leak: outside/leak_marker.txt was enumerated to the client (daemon's chdir followed the cd symlink during flist generation)" ++fi ++ ++exit 0 +diff --git a/util1.c b/util1.c +index 25ac7c9b..796604f6 100644 +--- a/util1.c ++++ b/util1.c +@@ -1116,6 +1116,7 @@ char *sanitize_path(char *dest, const char *p, const char *rootdir, int depth, i + * Also cleans the path using the clean_fname() function. */ + int change_dir(const char *dir, int set_path_only) + { ++ extern int am_daemon, am_chrooted; + static int initialised, skipped_chdir; + unsigned int len; + +@@ -1154,10 +1155,57 @@ int change_dir(const char *dir, int set_path_only) + curr_dir[curr_dir_len++] = '/'; + memcpy(curr_dir + curr_dir_len, dir, len + 1); + +- if (!set_path_only && chdir(curr_dir)) { +- curr_dir_len = save_dir_len; +- curr_dir[curr_dir_len] = '\0'; +- return 0; ++ if (!set_path_only) { ++ int chdir_failed; ++ /* In the daemon-without-chroot deployment we must not ++ * follow a symlink in any component of the chdir ++ * target -- otherwise CWD escapes the module and ++ * every subsequent path-relative syscall (open, ++ * chmod, lchown, ...) inherits the escape, which ++ * defeats secure_relative_open's RESOLVE_BENEATH ++ * anchor and re-opens the CVE-2026-29518 class of ++ * symlink TOCTOU attacks. Use the secure resolver ++ * to get a confined dirfd, then fchdir() to it. ++ * ++ * If skipped_chdir is set, a previous CD_SKIP_CHDIR ++ * call buffered an absolute prefix in curr_dir ++ * (e.g. change_pathname's CD_SKIP_CHDIR to orig_dir) ++ * without syncing the kernel's CWD. Resolve `dir` ++ * relative to that prefix as basedir so the secure ++ * branch still anchors at the operator-trusted ++ * directory rather than wherever the kernel CWD ++ * happens to be. */ ++ if (am_daemon && !am_chrooted) { ++ const char *basedir = NULL; ++ char prefix[MAXPATHLEN]; ++ int dfd; ++ if (skipped_chdir) { ++ if (save_dir_len >= sizeof prefix) { ++ errno = ENAMETOOLONG; ++ chdir_failed = 1; ++ goto chdir_cleanup; ++ } ++ memcpy(prefix, curr_dir, save_dir_len); ++ prefix[save_dir_len] = '\0'; ++ basedir = prefix; ++ } ++ dfd = secure_relative_open(basedir, dir, ++ O_RDONLY | O_DIRECTORY, 0); ++ if (dfd < 0) { ++ chdir_failed = 1; ++ } else { ++ chdir_failed = fchdir(dfd) != 0; ++ close(dfd); ++ } ++ } else { ++ chdir_failed = chdir(curr_dir) != 0; ++ } ++ chdir_cleanup: ++ if (chdir_failed) { ++ curr_dir_len = save_dir_len; ++ curr_dir[curr_dir_len] = '\0'; ++ return 0; ++ } + } + skipped_chdir = set_path_only; + } +-- +2.51.0 + diff -Nru rsync-3.2.7/debian/patches/2026-05-20/0041-syscall-add-symlink-race-safe-do_-_at-wrappers-and-h.patch rsync-3.2.7/debian/patches/2026-05-20/0041-syscall-add-symlink-race-safe-do_-_at-wrappers-and-h.patch --- rsync-3.2.7/debian/patches/2026-05-20/0041-syscall-add-symlink-race-safe-do_-_at-wrappers-and-h.patch 1970-01-01 00:00:00.000000000 +0000 +++ rsync-3.2.7/debian/patches/2026-05-20/0041-syscall-add-symlink-race-safe-do_-_at-wrappers-and-h.patch 2026-05-20 05:47:50.000000000 +0000 @@ -0,0 +1,1775 @@ +From 39b3074a1ab18705cd685fe0659fc958c8cd3db5 Mon Sep 17 00:00:00 2001 +From: Andrew Tridgell +Date: Tue, 5 May 2026 15:02:48 +1000 +Subject: [PATCH 41/56] syscall: add symlink-race-safe do_*_at() wrappers and + harden secure_relative_open + +Add the rest of the path-based syscall wrappers and migrate every +receiver-side caller: + - do_lchown_at, do_rename_at, do_mkdir_at, do_symlink_at, + do_mknod_at, do_link_at, do_unlink_at, do_rmdir_at, + do_utimensat_at, do_stat_at, do_lstat_at + +Same shape as do_chmod_at: open each parent under +secure_relative_open(), call the *at() variant against the dirfd, +fall through to the bare path-based syscall in non-daemon / +chrooted / absolute-path / no-parent cases. macOS's +setattrlist-based set_times tier is also routed through the +utimensat_at path on daemon-no-chroot. + +Hardenings to secure_relative_open() itself: + - confine basedir resolution under the same kernel mechanism + used for relpath (basedirs from --copy-dest / --link-dest are + sender-controllable in daemon mode) + - reject any '..' component (bare '..', 'foo/..', 'subdir/..') + so the per-component O_NOFOLLOW fallback can't escape + - return the dirfd we built up from the per-component fallback + when the caller passed O_DIRECTORY (otherwise every do_*_at + failed with EINVAL on platforms without RESOLVE_BENEATH) + +Adds testsuite/alt-dest-symlink-race.test and +testsuite/secure-relpath-validation.test (with t_secure_relpath +helper) as regression coverage for the new hardenings. + +Co-Authored-By: Claude Opus 4.7 (1M context) +--- + Makefile.in | 8 +- + backup.c | 14 +- + cleanup.c | 2 +- + delete.c | 2 +- + generator.c | 28 +- + hlink.c | 2 +- + receiver.c | 8 +- + rsync.c | 6 +- + syscall.c | 841 ++++++++++++++++++++++- + t_secure_relpath.c | 151 ++++ + testsuite/alt-dest-symlink-race.test | 96 +++ + testsuite/secure-relpath-validation.test | 34 + + util1.c | 20 +- + xattrs.c | 9 +- + 14 files changed, 1169 insertions(+), 52 deletions(-) + create mode 100644 t_secure_relpath.c + create mode 100755 testsuite/alt-dest-symlink-race.test + create mode 100755 testsuite/secure-relpath-validation.test + +diff --git a/Makefile.in b/Makefile.in +index 481f3ece..5c2b9a5a 100644 +--- a/Makefile.in ++++ b/Makefile.in +@@ -58,12 +58,12 @@ TLS_OBJ = tls.o syscall.o util2.o t_stub.o lib/compat.o lib/snprintf.o lib/perms + # Programs we must have to run the test cases + CHECK_PROGS = rsync$(EXEEXT) tls$(EXEEXT) getgroups$(EXEEXT) getfsdev$(EXEEXT) \ + testrun$(EXEEXT) trimslash$(EXEEXT) t_unsafe$(EXEEXT) t_chmod_secure$(EXEEXT) \ +- wildtest$(EXEEXT) simdtest$(EXEEXT) ++ t_secure_relpath$(EXEEXT) wildtest$(EXEEXT) simdtest$(EXEEXT) + + CHECK_SYMLINKS = testsuite/chown-fake.test testsuite/devices-fake.test testsuite/xattrs-hlink.test + + # Objects for CHECK_PROGS to clean +-CHECK_OBJS=tls.o testrun.o getgroups.o getfsdev.o t_stub.o t_unsafe.o t_chmod_secure.o trimslash.o wildtest.o ++CHECK_OBJS=tls.o testrun.o getgroups.o getfsdev.o t_stub.o t_unsafe.o t_chmod_secure.o t_secure_relpath.o trimslash.o wildtest.o + + # note that the -I. is needed to handle config.h when using VPATH + .c.o: +@@ -183,6 +183,10 @@ T_CHMOD_SECURE_OBJ = t_chmod_secure.o syscall.o util1.o util2.o t_stub.o lib/com + t_chmod_secure$(EXEEXT): $(T_CHMOD_SECURE_OBJ) + $(CC) $(CFLAGS) $(LDFLAGS) -o $@ $(T_CHMOD_SECURE_OBJ) $(LIBS) + ++T_SECURE_RELPATH_OBJ = t_secure_relpath.o syscall.o util1.o util2.o t_stub.o lib/compat.o lib/snprintf.o lib/wildmatch.o lib/permstring.o ++t_secure_relpath$(EXEEXT): $(T_SECURE_RELPATH_OBJ) ++ $(CC) $(CFLAGS) $(LDFLAGS) -o $@ $(T_SECURE_RELPATH_OBJ) $(LIBS) ++ + .PHONY: conf + conf: configure.sh config.h.in + +diff --git a/backup.c b/backup.c +index 686cb297..ae8cb49e 100644 +--- a/backup.c ++++ b/backup.c +@@ -39,7 +39,7 @@ static int validate_backup_dir(void) + { + STRUCT_STAT st; + +- if (do_lstat(backup_dir_buf, &st) < 0) { ++ if (do_lstat_at(backup_dir_buf, &st) < 0) { + if (errno == ENOENT) + return 0; + rsyserr(FERROR, errno, "backup lstat %s failed", backup_dir_buf); +@@ -98,7 +98,7 @@ static BOOL copy_valid_path(const char *fname) + for ( ; b; name = b + 1, b = strchr(name, '/')) { + *b = '\0'; + +- while (do_mkdir(backup_dir_buf, ACCESSPERMS) < 0) { ++ while (do_mkdir_at(backup_dir_buf, ACCESSPERMS) < 0) { + if (errno == EEXIST) { + val = validate_backup_dir(); + if (val > 0) +@@ -197,7 +197,7 @@ static inline int link_or_rename(const char *from, const char *to, + if (IS_SPECIAL(stp->st_mode) || IS_DEVICE(stp->st_mode)) + return 0; /* Use copy code. */ + #endif +- if (do_link(from, to) == 0) { ++ if (do_link_at(from, to) == 0) { + if (DEBUG_GTE(BACKUP, 1)) + rprintf(FINFO, "make_backup: HLINK %s successful.\n", from); + return 2; +@@ -207,7 +207,7 @@ static inline int link_or_rename(const char *from, const char *to, + return 0; + } + #endif +- if (do_rename(from, to) == 0) { ++ if (do_rename_at(from, to) == 0) { + if (stp->st_nlink > 1 && !S_ISDIR(stp->st_mode)) { + /* If someone has hard-linked the file into the backup + * dir, rename() might return success but do nothing! */ +@@ -246,7 +246,7 @@ int make_backup(const char *fname, BOOL prefer_rename) + goto success; + if (errno == EEXIST || errno == EISDIR) { + STRUCT_STAT bakst; +- if (do_lstat(buf, &bakst) == 0) { ++ if (do_lstat_at(buf, &bakst) == 0) { + int flags = get_del_for_flag(bakst.st_mode) | DEL_FOR_BACKUP | DEL_RECURSE; + if (delete_item(buf, bakst.st_mode, flags) != 0) + return 0; +@@ -277,7 +277,7 @@ int make_backup(const char *fname, BOOL prefer_rename) + /* Check to see if this is a device file, or link */ + if ((am_root && preserve_devices && IS_DEVICE(file->mode)) + || (preserve_specials && IS_SPECIAL(file->mode))) { +- if (do_mknod(buf, file->mode, sx.st.st_rdev) < 0) ++ if (do_mknod_at(buf, file->mode, sx.st.st_rdev) < 0) + rsyserr(FERROR, errno, "mknod %s failed", full_fname(buf)); + else if (DEBUG_GTE(BACKUP, 1)) + rprintf(FINFO, "make_backup: DEVICE %s successful.\n", fname); +@@ -294,7 +294,7 @@ int make_backup(const char *fname, BOOL prefer_rename) + } + ret = 2; + } else { +- if (do_symlink(sl, buf) < 0) ++ if (do_symlink_at(sl, buf) < 0) + rsyserr(FERROR, errno, "link %s -> \"%s\"", full_fname(buf), sl); + else if (DEBUG_GTE(BACKUP, 1)) + rprintf(FINFO, "make_backup: SYMLINK %s successful.\n", fname); +diff --git a/cleanup.c b/cleanup.c +index 40d26baa..0493fbbb 100644 +--- a/cleanup.c ++++ b/cleanup.c +@@ -198,7 +198,7 @@ NORETURN void _exit_cleanup(int code, const char *file, int line) + switch_step++; + + if (cleanup_fname) +- do_unlink(cleanup_fname); ++ do_unlink_at(cleanup_fname); + if (exit_code) + kill_all(SIGUSR1); + if (cleanup_pid && cleanup_pid == getpid()) { +diff --git a/delete.c b/delete.c +index 3a625610..3a82bc4c 100644 +--- a/delete.c ++++ b/delete.c +@@ -160,7 +160,7 @@ enum delret delete_item(char *fbuf, uint16 mode, uint16 flags) + + if (S_ISDIR(mode)) { + what = "rmdir"; +- ok = do_rmdir(fbuf) == 0; ++ ok = do_rmdir_at(fbuf) == 0; + } else { + if (make_backups > 0 && !(flags & DEL_FOR_BACKUP) && (backup_dir || !is_backup_file(fbuf))) { + what = "make_backup"; +diff --git a/generator.c b/generator.c +index c3ace1c2..a6bce20b 100644 +--- a/generator.c ++++ b/generator.c +@@ -984,7 +984,7 @@ static int try_dests_reg(struct file_struct *file, char *fname, int ndx, + if (find_exact_for_existing) { + if (alt_dest_type == LINK_DEST && real_st.st_dev == sxp->st.st_dev && real_st.st_ino == sxp->st.st_ino) + return -1; +- if (do_unlink(fname) < 0 && errno != ENOENT) ++ if (do_unlink_at(fname) < 0 && errno != ENOENT) + goto got_nothing_for_ya; + } + #ifdef SUPPORT_HARD_LINKS +@@ -1112,7 +1112,7 @@ static int try_dests_non(struct file_struct *file, char *fname, int ndx, + && !IS_SPECIAL(file->mode) && !IS_DEVICE(file->mode) + #endif + && !S_ISDIR(file->mode)) { +- if (do_link(cmpbuf, fname) < 0) { ++ if (do_link_at(cmpbuf, fname) < 0) { + rsyserr(FERROR_XFER, errno, + "failed to hard-link %s with %s", + cmpbuf, fname); +@@ -1315,7 +1315,7 @@ static void recv_generator(char *fname, struct file_struct *file, int ndx, + } + } + if (relative_paths && !implied_dirs && file->mode != 0 +- && do_stat(dn, &sx.st) < 0) { ++ && do_stat_at(dn, &sx.st) < 0) { + if (dry_run) + goto parent_is_dry_missing; + if (make_path(fname, MKP_DROP_NAME | MKP_SKIP_SLASH) < 0) { +@@ -1427,7 +1427,7 @@ static void recv_generator(char *fname, struct file_struct *file, int ndx, + && (stype == FT_DIR + || delete_item(fname, sx.st.st_mode, del_opts | DEL_FOR_DIR) != 0)) + goto cleanup; /* Any errors get reported later. */ +- if (do_mkdir(fname, (file->mode|added_perms) & 0700) == 0) ++ if (do_mkdir_at(fname, (file->mode|added_perms) & 0700) == 0) + file->flags |= FLAG_DIR_CREATED; + goto cleanup; + } +@@ -1469,10 +1469,10 @@ static void recv_generator(char *fname, struct file_struct *file, int ndx, + itemize(fnamecmp, file, ndx, statret, &sx, + statret ? ITEM_LOCAL_CHANGE : 0, 0, NULL); + } +- if (real_ret != 0 && do_mkdir(fname,file->mode|added_perms) < 0 && errno != EEXIST) { ++ if (real_ret != 0 && do_mkdir_at(fname,file->mode|added_perms) < 0 && errno != EEXIST) { + if (!relative_paths || errno != ENOENT + || make_path(fname, MKP_DROP_NAME | MKP_SKIP_SLASH) < 0 +- || (do_mkdir(fname, file->mode|added_perms) < 0 && errno != EEXIST)) { ++ || (do_mkdir_at(fname, file->mode|added_perms) < 0 && errno != EEXIST)) { + rsyserr(FERROR_XFER, errno, + "recv_generator: mkdir %s failed", + full_fname(fname)); +@@ -1808,7 +1808,7 @@ static void recv_generator(char *fname, struct file_struct *file, int ndx, + ; + else if (quick_check_ok(FT_REG, fnamecmp, file, &sx.st)) { + if (partialptr) { +- do_unlink(partialptr); ++ do_unlink_at(partialptr); + handle_partial_dir(partialptr, PDIR_DELETE); + } + set_file_attrs(fname, file, &sx, NULL, maybe_ATTRS_REPORT | maybe_ATTRS_ACCURATE_TIME); +@@ -2016,7 +2016,7 @@ int atomic_create(struct file_struct *file, char *fname, const char *slnk, const + + if (slnk) { + #ifdef SUPPORT_LINKS +- if (do_symlink(slnk, create_name) < 0) { ++ if (do_symlink_at(slnk, create_name) < 0) { + rsyserr(FERROR_XFER, errno, "symlink %s -> \"%s\" failed", + full_fname(create_name), slnk); + return 0; +@@ -2032,7 +2032,7 @@ int atomic_create(struct file_struct *file, char *fname, const char *slnk, const + return 0; + #endif + } else { +- if (do_mknod(create_name, file->mode, rdev) < 0) { ++ if (do_mknod_at(create_name, file->mode, rdev) < 0) { + rsyserr(FERROR_XFER, errno, "mknod %s failed", + full_fname(create_name)); + return 0; +@@ -2040,10 +2040,14 @@ int atomic_create(struct file_struct *file, char *fname, const char *slnk, const + } + + if (!skip_atomic) { +- if (do_rename(tmpname, fname) < 0) { ++ if (do_rename_at(tmpname, fname) < 0) { ++ char *full_tmpname = strdup(full_fname(tmpname)); ++ if (full_tmpname == NULL) ++ out_of_memory("atomic_create"); + rsyserr(FERROR_XFER, errno, "rename %s -> \"%s\" failed", +- full_fname(tmpname), full_fname(fname)); +- do_unlink(tmpname); ++ full_tmpname, full_fname(fname)); ++ free(full_tmpname); ++ do_unlink_at(tmpname); + return 0; + } + } +diff --git a/hlink.c b/hlink.c +index 2c14407a..eb36730f 100644 +--- a/hlink.c ++++ b/hlink.c +@@ -454,7 +454,7 @@ int hard_link_check(struct file_struct *file, int ndx, char *fname, + int hard_link_one(struct file_struct *file, const char *fname, + const char *oldname, int terse) + { +- if (do_link(oldname, fname) < 0) { ++ if (do_link_at(oldname, fname) < 0) { + enum logcode code; + if (terse) { + if (!INFO_GTE(NAME, 1)) +diff --git a/receiver.c b/receiver.c +index cbe18196..8f5b51dd 100644 +--- a/receiver.c ++++ b/receiver.c +@@ -442,7 +442,7 @@ static void handle_delayed_updates(char *local_name) + } + /* We don't use robust_rename() here because the + * partial-dir must be on the same drive. */ +- if (do_rename(partialptr, fname) < 0) { ++ if (do_rename_at(partialptr, fname) < 0) { + rsyserr(FERROR_XFER, errno, + "rename failed for %s (from %s)", + full_fname(fname), partialptr); +@@ -926,7 +926,7 @@ int recv_files(int f_in, int f_out, char *local_name) + recv_ok = -1; + else if (fnamecmp == partialptr) { + if (!one_inplace) +- do_unlink(partialptr); ++ do_unlink_at(partialptr); + handle_partial_dir(partialptr, PDIR_DELETE); + } + } else if (keep_partial && partialptr && (!one_inplace || delay_updates)) { +@@ -935,7 +935,7 @@ int recv_files(int f_in, int f_out, char *local_name) + "Unable to create partial-dir for %s -- discarding %s.\n", + local_name ? local_name : f_name(file, NULL), + recv_ok ? "completed file" : "partial file"); +- do_unlink(fnametmp); ++ do_unlink_at(fnametmp); + recv_ok = -1; + } else if (!finish_transfer(partialptr, fnametmp, fnamecmp, NULL, + file, recv_ok, !partial_dir)) +@@ -946,7 +946,7 @@ int recv_files(int f_in, int f_out, char *local_name) + } else + partialptr = NULL; + } else if (!one_inplace) +- do_unlink(fnametmp); ++ do_unlink_at(fnametmp); + + cleanup_disable(); + +diff --git a/rsync.c b/rsync.c +index cc46a2f9..1d2ae82a 100644 +--- a/rsync.c ++++ b/rsync.c +@@ -547,7 +547,7 @@ int set_file_attrs(const char *fname, struct file_struct *file, stat_x *sxp, + if (am_root >= 0) { + uid_t uid = change_uid ? (uid_t)F_OWNER(file) : sxp->st.st_uid; + gid_t gid = change_gid ? (gid_t)F_GROUP(file) : sxp->st.st_gid; +- if (do_lchown(fname, uid, gid) != 0) { ++ if (do_lchown_at(fname, uid, gid) != 0) { + /* We shouldn't have attempted to change uid + * or gid unless have the privilege. */ + rsyserr(FERROR_XFER, errno, "%s %s failed", +@@ -758,7 +758,7 @@ int finish_transfer(const char *fname, const char *fnametmp, + full_fname(fnametmp), fname); + if (!partialptr || (ret == -2 && temp_copy_name) + || robust_rename(fnametmp, partialptr, NULL, file->mode) < 0) +- do_unlink(fnametmp); ++ do_unlink_at(fnametmp); + return 0; + } + if (ret == 0) { +@@ -774,7 +774,7 @@ int finish_transfer(const char *fname, const char *fnametmp, + ok_to_set_time ? ATTRS_ACCURATE_TIME : ATTRS_SKIP_MTIME | ATTRS_SKIP_ATIME | ATTRS_SKIP_CRTIME); + + if (temp_copy_name) { +- if (do_rename(fnametmp, fname) < 0) { ++ if (do_rename_at(fnametmp, fname) < 0) { + rsyserr(FERROR_XFER, errno, "rename %s -> \"%s\"", + full_fname(fnametmp), fname); + return 0; +diff --git a/syscall.c b/syscall.c +index 167aae0e..2cff0b38 100644 +--- a/syscall.c ++++ b/syscall.c +@@ -93,6 +93,63 @@ int do_unlink(const char *path) + return unlink(path); + } + ++/* ++ Symlink-race-safe variant of do_unlink() for receiver-side use. See ++ the comment on do_chmod_at() for the threat model. unlink() resolves ++ parent components, so a parent-symlink swap can delete an outside ++ file under the daemon's authority. Defence: open the parent of path ++ under secure_relative_open() and use unlinkat() (flags=0) against ++ that dirfd. ++ ++ Falls through to do_unlink() for the same dry-run / non-daemon / ++ chrooted / no-parent / absolute-path cases as the other wrappers. ++*/ ++int do_unlink_at(const char *path) ++{ ++#ifdef AT_FDCWD ++ extern int am_daemon, am_chrooted; ++ char dirpath[MAXPATHLEN]; ++ const char *bname; ++ const char *slash; ++ int dfd, ret, e; ++ size_t dlen; ++ ++ if (dry_run) return 0; ++ RETURN_ERROR_IF_RO_OR_LO; ++ ++ if (!am_daemon || am_chrooted) ++ return unlink(path); ++ ++ if (!path || !*path || *path == '/') ++ return unlink(path); ++ ++ slash = strrchr(path, '/'); ++ if (!slash) ++ return unlink(path); ++ ++ dlen = slash - path; ++ if (dlen >= sizeof dirpath) { ++ errno = ENAMETOOLONG; ++ return -1; ++ } ++ memcpy(dirpath, path, dlen); ++ dirpath[dlen] = '\0'; ++ bname = slash + 1; ++ ++ dfd = secure_relative_open(NULL, dirpath, O_RDONLY | O_DIRECTORY, 0); ++ if (dfd < 0) ++ return -1; ++ ++ ret = unlinkat(dfd, bname, 0); ++ e = errno; ++ close(dfd); ++ errno = e; ++ return ret; ++#else ++ return do_unlink(path); ++#endif ++} ++ + #ifdef SUPPORT_LINKS + int do_symlink(const char *lnk, const char *path) + { +@@ -117,6 +174,70 @@ int do_symlink(const char *lnk, const char *path) + return symlink(lnk, path); + } + ++/* ++ Symlink-race-safe variant of do_symlink() for receiver-side use. See ++ the comment on do_chmod_at() for the threat model. Only the parent ++ directory of `path` needs protection -- symlinkat() does not resolve ++ the final component (it creates it). Defence: open parent of `path` ++ under secure_relative_open() and call symlinkat() against that ++ dirfd. The link target string `lnk` is stored verbatim and not ++ resolved at creation time, so it doesn't need scrutiny here. ++ ++ Falls through to do_symlink() for the --fake-super (am_root < 0) ++ path -- that code path opens `path` with do_open() which has its ++ own (separate) symlink-race exposure tracked elsewhere. ++*/ ++int do_symlink_at(const char *lnk, const char *path) ++{ ++#ifdef AT_FDCWD ++ extern int am_daemon, am_chrooted; ++ char dirpath[MAXPATHLEN]; ++ const char *bname; ++ const char *slash; ++ int dfd, ret, e; ++ size_t dlen; ++ ++ if (dry_run) return 0; ++ RETURN_ERROR_IF_RO_OR_LO; ++ ++ if (!am_daemon || am_chrooted) ++ return do_symlink(lnk, path); ++ ++#if defined NO_SYMLINK_XATTRS || defined NO_SYMLINK_USER_XATTRS ++ if (am_root < 0) ++ return do_symlink(lnk, path); ++#endif ++ ++ if (!path || !*path || *path == '/') ++ return do_symlink(lnk, path); ++ ++ slash = strrchr(path, '/'); ++ if (!slash) ++ return do_symlink(lnk, path); ++ ++ dlen = slash - path; ++ if (dlen >= sizeof dirpath) { ++ errno = ENAMETOOLONG; ++ return -1; ++ } ++ memcpy(dirpath, path, dlen); ++ dirpath[dlen] = '\0'; ++ bname = slash + 1; ++ ++ dfd = secure_relative_open(NULL, dirpath, O_RDONLY | O_DIRECTORY, 0); ++ if (dfd < 0) ++ return -1; ++ ++ ret = symlinkat(lnk, dfd, bname); ++ e = errno; ++ close(dfd); ++ errno = e; ++ return ret; ++#else ++ return do_symlink(lnk, path); ++#endif ++} ++ + #if defined NO_SYMLINK_XATTRS || defined NO_SYMLINK_USER_XATTRS + ssize_t do_readlink(const char *path, char *buf, size_t bufsiz) + { +@@ -153,6 +274,106 @@ int do_link(const char *old_path, const char *new_path) + return link(old_path, new_path); + #endif + } ++ ++/* ++ Symlink-race-safe variant of do_link() for receiver-side use. See ++ the comment on do_chmod_at() for the threat model. link() resolves ++ parent components of *both* old_path and new_path, so a parent- ++ symlink swap on either side can plant the new hard link outside ++ the module, or hard-link an outside file into the module (read ++ disclosure). ++ ++ Defence: open each parent under secure_relative_open() and use ++ linkat() between the two dirfds, reusing one when the parents ++ match. flags=0 matches the existing do_link() (don't follow a ++ symbolic-link old_path). Only available on systems with linkat(); ++ pre-AT_FDCWD systems fall through to do_link(). ++*/ ++int do_link_at(const char *old_path, const char *new_path) ++{ ++#if defined AT_FDCWD && defined HAVE_LINKAT ++ extern int am_daemon, am_chrooted; ++ char old_dirpath[MAXPATHLEN], new_dirpath[MAXPATHLEN]; ++ const char *old_bname, *new_bname; ++ const char *old_slash, *new_slash; ++ int old_dfd = AT_FDCWD, new_dfd = AT_FDCWD; ++ BOOL old_owns = False, new_owns = False; ++ int ret, e; ++ size_t old_dlen = 0, new_dlen = 0; ++ ++ if (dry_run) return 0; ++ RETURN_ERROR_IF_RO_OR_LO; ++ ++ if (!am_daemon || am_chrooted) ++ return do_link(old_path, new_path); ++ ++ if (!old_path || !*old_path || *old_path == '/' ++ || !new_path || !*new_path || *new_path == '/') ++ return do_link(old_path, new_path); ++ ++ old_slash = strrchr(old_path, '/'); ++ new_slash = strrchr(new_path, '/'); ++ ++ /* Resolve each path's parent dir independently. A path without a ++ * slash lives in CWD (AT_FDCWD), no parent open required. A path ++ * with a slash needs secure_relative_open to confine its parent ++ * resolution -- otherwise a parent symlink (e.g. --link-dest=cd ++ * where cd -> /outside) lets the kernel-level linkat(AT_FDCWD, ++ * "cd/target.txt", ...) escape the module. */ ++ if (old_slash) { ++ old_dlen = old_slash - old_path; ++ if (old_dlen >= sizeof old_dirpath) { errno = ENAMETOOLONG; return -1; } ++ memcpy(old_dirpath, old_path, old_dlen); ++ old_dirpath[old_dlen] = '\0'; ++ old_bname = old_slash + 1; ++ old_dfd = secure_relative_open(NULL, old_dirpath, O_RDONLY | O_DIRECTORY, 0); ++ if (old_dfd < 0) ++ return -1; ++ old_owns = True; ++ } else { ++ old_bname = old_path; ++ } ++ ++ if (new_slash) { ++ new_dlen = new_slash - new_path; ++ if (new_dlen >= sizeof new_dirpath) { ++ e = ENAMETOOLONG; ++ if (old_owns) close(old_dfd); ++ errno = e; ++ return -1; ++ } ++ memcpy(new_dirpath, new_path, new_dlen); ++ new_dirpath[new_dlen] = '\0'; ++ new_bname = new_slash + 1; ++ if (old_owns && old_dlen == new_dlen ++ && memcmp(old_dirpath, new_dirpath, old_dlen) == 0) { ++ new_dfd = old_dfd; ++ } else { ++ new_dfd = secure_relative_open(NULL, new_dirpath, O_RDONLY | O_DIRECTORY, 0); ++ if (new_dfd < 0) { ++ e = errno; ++ if (old_owns) close(old_dfd); ++ errno = e; ++ return -1; ++ } ++ new_owns = True; ++ } ++ } else { ++ new_bname = new_path; ++ } ++ ++ ret = linkat(old_dfd, old_bname, new_dfd, new_bname, 0); ++ e = errno; ++ if (new_owns) ++ close(new_dfd); ++ if (old_owns) ++ close(old_dfd); ++ errno = e; ++ return ret; ++#else ++ return do_link(old_path, new_path); ++#endif ++} + #endif + + int do_lchown(const char *path, uid_t owner, gid_t group) +@@ -165,6 +386,66 @@ int do_lchown(const char *path, uid_t owner, gid_t group) + return lchown(path, owner, group); + } + ++/* ++ Symlink-race-safe variant of do_lchown() for receiver-side use. See the ++ comment on do_chmod_at() for the threat model and design rationale. ++ ++ Resolves the parent directory under secure_relative_open() and invokes ++ fchownat(..., AT_SYMLINK_NOFOLLOW) against that dirfd, so that an ++ attacker who substitutes a symlink into one of the parent components ++ cannot redirect the chown outside the receiver's confinement. The ++ AT_SYMLINK_NOFOLLOW flag matches lchown()'s "do not follow a final- ++ component symlink" semantics. ++ ++ Falls through to do_lchown() in the dry-run / non-daemon / chrooted / ++ absolute-path / no-parent cases, identical to do_chmod_at(). ++*/ ++int do_lchown_at(const char *fname, uid_t owner, gid_t group) ++{ ++#ifdef AT_FDCWD ++ extern int am_daemon, am_chrooted; ++ char dirpath[MAXPATHLEN]; ++ const char *bname; ++ const char *slash; ++ int dfd, ret, e; ++ size_t dlen; ++ ++ if (dry_run) return 0; ++ RETURN_ERROR_IF_RO_OR_LO; ++ ++ if (!am_daemon || am_chrooted) ++ return do_lchown(fname, owner, group); ++ ++ if (!fname || !*fname || *fname == '/') ++ return do_lchown(fname, owner, group); ++ ++ slash = strrchr(fname, '/'); ++ if (!slash) ++ return do_lchown(fname, owner, group); ++ ++ dlen = slash - fname; ++ if (dlen >= sizeof dirpath) { ++ errno = ENAMETOOLONG; ++ return -1; ++ } ++ memcpy(dirpath, fname, dlen); ++ dirpath[dlen] = '\0'; ++ bname = slash + 1; ++ ++ dfd = secure_relative_open(NULL, dirpath, O_RDONLY | O_DIRECTORY, 0); ++ if (dfd < 0) ++ return -1; ++ ++ ret = fchownat(dfd, bname, owner, group, AT_SYMLINK_NOFOLLOW); ++ e = errno; ++ close(dfd); ++ errno = e; ++ return ret; ++#else ++ return do_lchown(fname, owner, group); ++#endif ++} ++ + int do_mknod(const char *pathname, mode_t mode, dev_t dev) + { + if (dry_run) return 0; +@@ -215,6 +496,76 @@ int do_mknod(const char *pathname, mode_t mode, dev_t dev) + #endif + } + ++/* ++ Symlink-race-safe variant of do_mknod() for receiver-side use. See ++ the comment on do_chmod_at() for the threat model. Defence: open ++ the parent of pathname under secure_relative_open() and use ++ mknodat() against that dirfd. mknodat() covers both regular-file ++ (S_IFREG with dev=0) and FIFO (S_IFIFO) and device-node creation. ++ ++ Falls through to do_mknod() for fake-super (am_root < 0) and for ++ sockets, both of which use auxiliary path-based syscalls that ++ don't have an *at() variant in any portable form. ++*/ ++int do_mknod_at(const char *pathname, mode_t mode, dev_t dev) ++{ ++#ifdef AT_FDCWD ++ extern int am_daemon, am_chrooted; ++ char dirpath[MAXPATHLEN]; ++ const char *bname; ++ const char *slash; ++ int dfd, ret, e; ++ size_t dlen; ++ ++ if (dry_run) return 0; ++ RETURN_ERROR_IF_RO_OR_LO; ++ ++ if (!am_daemon || am_chrooted) ++ return do_mknod(pathname, mode, dev); ++ ++ if (am_root < 0) ++ return do_mknod(pathname, mode, dev); ++ ++#if !defined MKNOD_CREATES_SOCKETS && defined HAVE_SYS_UN_H ++ if (S_ISSOCK(mode)) ++ return do_mknod(pathname, mode, dev); ++#endif ++ ++ if (!pathname || !*pathname || *pathname == '/') ++ return do_mknod(pathname, mode, dev); ++ ++ slash = strrchr(pathname, '/'); ++ if (!slash) ++ return do_mknod(pathname, mode, dev); ++ ++ dlen = slash - pathname; ++ if (dlen >= sizeof dirpath) { ++ errno = ENAMETOOLONG; ++ return -1; ++ } ++ memcpy(dirpath, pathname, dlen); ++ dirpath[dlen] = '\0'; ++ bname = slash + 1; ++ ++ dfd = secure_relative_open(NULL, dirpath, O_RDONLY | O_DIRECTORY, 0); ++ if (dfd < 0) ++ return -1; ++ ++#if !defined MKNOD_CREATES_FIFOS && defined HAVE_MKFIFO ++ if (S_ISFIFO(mode)) ++ ret = mkfifoat(dfd, bname, mode); ++ else ++#endif ++ ret = mknodat(dfd, bname, mode, dev); ++ e = errno; ++ close(dfd); ++ errno = e; ++ return ret; ++#else ++ return do_mknod(pathname, mode, dev); ++#endif ++} ++ + int do_rmdir(const char *pathname) + { + if (dry_run) return 0; +@@ -222,6 +573,57 @@ int do_rmdir(const char *pathname) + return rmdir(pathname); + } + ++/* ++ Symlink-race-safe variant of do_rmdir(). See do_unlink_at() above; ++ same shape but with AT_REMOVEDIR set to require the target be a ++ directory. ++*/ ++int do_rmdir_at(const char *pathname) ++{ ++#ifdef AT_FDCWD ++ extern int am_daemon, am_chrooted; ++ char dirpath[MAXPATHLEN]; ++ const char *bname; ++ const char *slash; ++ int dfd, ret, e; ++ size_t dlen; ++ ++ if (dry_run) return 0; ++ RETURN_ERROR_IF_RO_OR_LO; ++ ++ if (!am_daemon || am_chrooted) ++ return rmdir(pathname); ++ ++ if (!pathname || !*pathname || *pathname == '/') ++ return rmdir(pathname); ++ ++ slash = strrchr(pathname, '/'); ++ if (!slash) ++ return rmdir(pathname); ++ ++ dlen = slash - pathname; ++ if (dlen >= sizeof dirpath) { ++ errno = ENAMETOOLONG; ++ return -1; ++ } ++ memcpy(dirpath, pathname, dlen); ++ dirpath[dlen] = '\0'; ++ bname = slash + 1; ++ ++ dfd = secure_relative_open(NULL, dirpath, O_RDONLY | O_DIRECTORY, 0); ++ if (dfd < 0) ++ return -1; ++ ++ ret = unlinkat(dfd, bname, AT_REMOVEDIR); ++ e = errno; ++ close(dfd); ++ errno = e; ++ return ret; ++#else ++ return do_rmdir(pathname); ++#endif ++} ++ + int do_open(const char *pathname, int flags, mode_t mode) + { + if (flags != O_RDONLY) { +@@ -370,6 +772,89 @@ int do_rename(const char *old_path, const char *new_path) + return rename(old_path, new_path); + } + ++/* ++ Symlink-race-safe variant of do_rename() for receiver-side use. See ++ the comment on do_chmod_at() for the threat model and design rationale. ++ ++ rename() is the central tmp -> final operation in rsync; if either the ++ source or the destination has an attacker-substituted symlink in one ++ of its parent components, the rename can publish or vanish files ++ outside the module. Defence: open the parent of *each* path under ++ secure_relative_open() and use renameat() against the resulting ++ dirfds. When old_path and new_path share the same parent (the common ++ case -- tmp file living next to its final name), we reuse the same ++ dirfd for both sides. ++ ++ Falls through to do_rename() in dry-run, non-daemon, chrooted, no- ++ parent and absolute-path cases, identical to the other do_*_at() ++ wrappers. ++*/ ++int do_rename_at(const char *old_path, const char *new_path) ++{ ++#ifdef AT_FDCWD ++ extern int am_daemon, am_chrooted; ++ char old_dirpath[MAXPATHLEN], new_dirpath[MAXPATHLEN]; ++ const char *old_bname, *new_bname; ++ const char *old_slash, *new_slash; ++ int old_dfd = -1, new_dfd = -1, ret = -1, e; ++ size_t old_dlen, new_dlen; ++ ++ if (dry_run) return 0; ++ RETURN_ERROR_IF_RO_OR_LO; ++ ++ if (!am_daemon || am_chrooted) ++ return do_rename(old_path, new_path); ++ ++ if (!old_path || !*old_path || *old_path == '/' ++ || !new_path || !*new_path || *new_path == '/') ++ return do_rename(old_path, new_path); ++ ++ old_slash = strrchr(old_path, '/'); ++ new_slash = strrchr(new_path, '/'); ++ if (!old_slash || !new_slash) ++ return do_rename(old_path, new_path); ++ ++ old_dlen = old_slash - old_path; ++ new_dlen = new_slash - new_path; ++ if (old_dlen >= sizeof old_dirpath || new_dlen >= sizeof new_dirpath) { ++ errno = ENAMETOOLONG; ++ return -1; ++ } ++ memcpy(old_dirpath, old_path, old_dlen); ++ old_dirpath[old_dlen] = '\0'; ++ memcpy(new_dirpath, new_path, new_dlen); ++ new_dirpath[new_dlen] = '\0'; ++ old_bname = old_slash + 1; ++ new_bname = new_slash + 1; ++ ++ old_dfd = secure_relative_open(NULL, old_dirpath, O_RDONLY | O_DIRECTORY, 0); ++ if (old_dfd < 0) ++ return -1; ++ ++ if (old_dlen == new_dlen && memcmp(old_dirpath, new_dirpath, old_dlen) == 0) { ++ new_dfd = old_dfd; ++ } else { ++ new_dfd = secure_relative_open(NULL, new_dirpath, O_RDONLY | O_DIRECTORY, 0); ++ if (new_dfd < 0) { ++ e = errno; ++ close(old_dfd); ++ errno = e; ++ return -1; ++ } ++ } ++ ++ ret = renameat(old_dfd, old_bname, new_dfd, new_bname); ++ e = errno; ++ if (new_dfd != old_dfd) ++ close(new_dfd); ++ close(old_dfd); ++ errno = e; ++ return ret; ++#else ++ return do_rename(old_path, new_path); ++#endif ++} ++ + #ifdef HAVE_FTRUNCATE + int do_ftruncate(int fd, OFF_T size) + { +@@ -412,6 +897,66 @@ int do_mkdir(char *path, mode_t mode) + return mkdir(path, mode); + } + ++/* ++ Symlink-race-safe variant of do_mkdir() for receiver-side use. See ++ the comment on do_chmod_at() for the threat model and design rationale. ++ ++ mkdir() resolves parent symlinks at every component, so a parent- ++ component swap can place an attacker-named directory outside the ++ module. Defence: open the parent of fname under secure_relative_open() ++ and call mkdirat() against that dirfd. ++ ++ Mutates path in place to trim trailing slashes (matches do_mkdir()). ++ Falls through to do_mkdir() in dry-run, non-daemon, chrooted, no- ++ parent and absolute-path cases. ++*/ ++int do_mkdir_at(char *path, mode_t mode) ++{ ++#ifdef AT_FDCWD ++ extern int am_daemon, am_chrooted; ++ char dirpath[MAXPATHLEN]; ++ const char *bname; ++ const char *slash; ++ int dfd, ret, e; ++ size_t dlen; ++ ++ if (dry_run) return 0; ++ RETURN_ERROR_IF_RO_OR_LO; ++ trim_trailing_slashes(path); ++ ++ if (!am_daemon || am_chrooted) ++ return mkdir(path, mode); ++ ++ if (!path || !*path || *path == '/') ++ return mkdir(path, mode); ++ ++ slash = strrchr(path, '/'); ++ if (!slash) ++ return mkdir(path, mode); ++ ++ dlen = slash - path; ++ if (dlen >= sizeof dirpath) { ++ errno = ENAMETOOLONG; ++ return -1; ++ } ++ memcpy(dirpath, path, dlen); ++ dirpath[dlen] = '\0'; ++ bname = slash + 1; ++ ++ dfd = secure_relative_open(NULL, dirpath, O_RDONLY | O_DIRECTORY, 0); ++ if (dfd < 0) ++ return -1; ++ ++ ret = mkdirat(dfd, bname, mode); ++ e = errno; ++ close(dfd); ++ errno = e; ++ return ret; ++#else ++ return do_mkdir(path, mode); ++#endif ++} ++ + /* like mkstemp but forces permissions */ + int do_mkstemp(char *template, mode_t perms) + { +@@ -465,6 +1010,76 @@ int do_lstat(const char *path, STRUCT_STAT *st) + #endif + } + ++/* ++ Symlink-race-safe variants of do_stat() / do_lstat() for receiver- ++ side use. See the comment on do_chmod_at() for the threat model. ++ stat() and lstat() resolve parent components, so a parent-symlink ++ swap can make the receiver's stat see attributes of a victim file ++ outside the module -- which then drives later behaviour (e.g. ++ "this isn't a directory, delete it" -> attacker-controlled unlink ++ on something outside the module). ++ ++ Defence: open the parent under secure_relative_open() and use ++ fstatat() with AT_SYMLINK_NOFOLLOW (lstat) or 0 (stat) against ++ that dirfd. Same fall-through gating as the other wrappers. ++*/ ++static int do_xstat_at(const char *path, STRUCT_STAT *st, int at_flags, int (*fallback)(const char *, STRUCT_STAT *)) ++{ ++#ifdef AT_FDCWD ++ extern int am_daemon, am_chrooted; ++ char dirpath[MAXPATHLEN]; ++ const char *bname; ++ const char *slash; ++ int dfd, ret, e; ++ size_t dlen; ++ ++ if (!am_daemon || am_chrooted) ++ return fallback(path, st); ++ ++ if (!path || !*path || *path == '/') ++ return fallback(path, st); ++ ++ slash = strrchr(path, '/'); ++ if (!slash) ++ return fallback(path, st); ++ ++ dlen = slash - path; ++ if (dlen >= sizeof dirpath) { ++ errno = ENAMETOOLONG; ++ return -1; ++ } ++ memcpy(dirpath, path, dlen); ++ dirpath[dlen] = '\0'; ++ bname = slash + 1; ++ ++ dfd = secure_relative_open(NULL, dirpath, O_RDONLY | O_DIRECTORY, 0); ++ if (dfd < 0) ++ return -1; ++ ++ ret = fstatat(dfd, bname, st, at_flags); ++ e = errno; ++ close(dfd); ++ errno = e; ++ return ret; ++#else ++ return fallback(path, st); ++#endif ++} ++ ++int do_stat_at(const char *path, STRUCT_STAT *st) ++{ ++ return do_xstat_at(path, st, 0, do_stat); ++} ++ ++int do_lstat_at(const char *path, STRUCT_STAT *st) ++{ ++#ifdef SUPPORT_LINKS ++ return do_xstat_at(path, st, AT_SYMLINK_NOFOLLOW, do_lstat); ++#else ++ return do_xstat_at(path, st, 0, do_stat); ++#endif ++} ++ + int do_fstat(int fd, STRUCT_STAT *st) + { + #ifdef USE_STAT64_FUNCS +@@ -486,12 +1101,26 @@ OFF_T do_lseek(int fd, OFF_T offset, int whence) + #ifdef HAVE_SETATTRLIST + int do_setattrlist_times(const char *path, STRUCT_STAT *stp) + { ++ extern int am_daemon, am_chrooted; + struct attrlist attrList; + struct timespec ts[2]; + + if (dry_run) return 0; + RETURN_ERROR_IF_RO_OR_LO; + ++ /* setattrlist() takes a raw path and follows parent symlinks ++ * (FSOPT_NOFOLLOW only blocks the final component). On a ++ * daemon-no-chroot deployment, return ENOSYS so set_times()' ++ * tier walk falls through to do_utimensat_at(), which routes ++ * the timestamp update through a secure parent dirfd. The ++ * macOS-specific attribute set this function would have used ++ * (ATTR_CMN_MODTIME / ATTR_CMN_ACCTIME) is the same set ++ * utimensat() handles, so no functionality is lost. */ ++ if (am_daemon && !am_chrooted) { ++ errno = ENOSYS; ++ return -1; ++ } ++ + /* Yes, this is in the opposite order of utime and similar. */ + ts[0].tv_sec = stp->st_mtime; + ts[0].tv_nsec = stp->ST_MTIME_NSEC; +@@ -508,12 +1137,25 @@ int do_setattrlist_times(const char *path, STRUCT_STAT *stp) + #ifdef SUPPORT_CRTIMES + int do_setattrlist_crtime(const char *path, time_t crtime) + { ++ extern int am_daemon, am_chrooted; + struct attrlist attrList; + struct timespec ts; + + if (dry_run) return 0; + RETURN_ERROR_IF_RO_OR_LO; + ++ /* Same path-follows-parent-symlinks concern as ++ * do_setattrlist_times. There is no portable at-aware variant ++ * of setattrlist that targets ATTR_CMN_CRTIME, so on a ++ * daemon-no-chroot deployment we return -1 and accept that ++ * crtime preservation is silently dropped for that file (the ++ * caller treats this as "crtime not updated"). The transfer ++ * itself continues normally. */ ++ if (am_daemon && !am_chrooted) { ++ errno = ENOSYS; ++ return -1; ++ } ++ + ts.tv_sec = crtime; + ts.tv_nsec = 0; + +@@ -529,10 +1171,19 @@ int do_setattrlist_crtime(const char *path, time_t crtime) + time_t get_create_time(const char *path, STRUCT_STAT *stp) + { + #ifdef HAVE_GETATTRLIST ++ extern int am_daemon, am_chrooted; + static struct create_time attrBuf; + struct attrlist attrList; + + (void)stp; ++ /* getattrlist() is also path-based and follows parent ++ * symlinks. In daemon-no-chroot, refuse rather than read the ++ * crtime of a file the parent-symlink chain might point at ++ * outside the module. The caller's "no crtime available" ++ * path returns 0; the file gets a fresh crtime instead of ++ * preserving the source's. */ ++ if (am_daemon && !am_chrooted) ++ return 0; + memset(&attrList, 0, sizeof attrList); + attrList.bitmapcount = ATTR_BIT_MAP_COUNT; + attrList.commonattr = ATTR_CMN_CRTIME; +@@ -598,6 +1249,81 @@ int do_utimensat(const char *path, STRUCT_STAT *stp) + #endif + return utimensat(AT_FDCWD, path, t, AT_SYMLINK_NOFOLLOW); + } ++ ++/* ++ Symlink-race-safe variant of do_utimensat() for receiver-side use. ++ See the comment on do_chmod_at() for the threat model. utimes() ++ resolves parent components and follows a final-component symlink; ++ lutimes() doesn't follow the final component but still resolves ++ parents. Either way, a parent-symlink swap can redirect the ++ timestamp update outside the module. Defence: open the parent of ++ path under secure_relative_open() and call utimensat() with ++ AT_SYMLINK_NOFOLLOW against that dirfd. ++ ++ Falls through to do_utimensat() in the same dry-run / non-daemon / ++ chrooted / no-parent / absolute-path cases as the other wrappers. ++ Returns -1 with errno=ENOSYS on systems without utimensat() ++ (caller is expected to fall back to the legacy tier walk). ++*/ ++int do_utimensat_at(const char *path, STRUCT_STAT *stp) ++{ ++#ifdef AT_FDCWD ++ extern int am_daemon, am_chrooted; ++ struct timespec t[2]; ++ char dirpath[MAXPATHLEN]; ++ const char *bname; ++ const char *slash; ++ int dfd, ret, e; ++ size_t dlen; ++ ++ if (dry_run) return 0; ++ RETURN_ERROR_IF_RO_OR_LO; ++ ++ if (!am_daemon || am_chrooted) ++ return do_utimensat(path, stp); ++ ++ if (!path || !*path || *path == '/') ++ return do_utimensat(path, stp); ++ ++ slash = strrchr(path, '/'); ++ if (!slash) ++ return do_utimensat(path, stp); ++ ++ dlen = slash - path; ++ if (dlen >= sizeof dirpath) { ++ errno = ENAMETOOLONG; ++ return -1; ++ } ++ memcpy(dirpath, path, dlen); ++ dirpath[dlen] = '\0'; ++ bname = slash + 1; ++ ++ t[0].tv_sec = stp->st_atime; ++#ifdef ST_ATIME_NSEC ++ t[0].tv_nsec = stp->ST_ATIME_NSEC; ++#else ++ t[0].tv_nsec = 0; ++#endif ++ t[1].tv_sec = stp->st_mtime; ++#ifdef ST_MTIME_NSEC ++ t[1].tv_nsec = stp->ST_MTIME_NSEC; ++#else ++ t[1].tv_nsec = 0; ++#endif ++ ++ dfd = secure_relative_open(NULL, dirpath, O_RDONLY | O_DIRECTORY, 0); ++ if (dfd < 0) ++ return -1; ++ ++ ret = utimensat(dfd, bname, t, AT_SYMLINK_NOFOLLOW); ++ e = errno; ++ close(dfd); ++ errno = e; ++ return ret; ++#else ++ return do_utimensat(path, stp); ++#endif ++} + #endif + + #ifdef HAVE_LUTIMES +@@ -820,6 +1546,30 @@ int do_open_nofollow(const char *pathname, int flags) + The relpath must also not contain any ../ elements in the path. + */ + ++/* Returns 1 if path has any "/"-separated component that is exactly ++ * "..", 0 otherwise. Used by secure_relative_open's front-door ++ * validation to reject inputs that the per-component walk fallback ++ * would otherwise resolve through ".." -- e.g. bare "..", "foo/..", ++ * "subdir/.." -- which RESOLVE_BENEATH-equivalent kernels reject in ++ * the kernel but the per-component fallback (NetBSD/OpenBSD/Solaris/ ++ * Cygwin/pre-5.6 Linux) does not. */ ++static int path_has_dotdot_component(const char *path) ++{ ++ const char *p = path; ++ ++ while (*p) { ++ const char *q; ++ if (*p == '/') { p++; continue; } ++ q = p; ++ while (*q && *q != '/') ++ q++; ++ if (q - p == 2 && p[0] == '.' && p[1] == '.') ++ return 1; ++ p = q; ++ } ++ return 0; ++} ++ + #ifdef __linux__ + static int secure_relative_open_linux(const char *basedir, const char *relpath, int flags, mode_t mode) + { +@@ -833,10 +1583,25 @@ static int secure_relative_open_linux(const char *basedir, const char *relpath, + + if (basedir == NULL) { + dirfd = AT_FDCWD; +- } else { ++ } else if (basedir[0] == '/') { ++ /* Absolute basedir: operator-trusted (module_dir and the ++ * like). Plain openat. */ + dirfd = openat(AT_FDCWD, basedir, O_RDONLY | O_DIRECTORY); + if (dirfd == -1) + return -1; ++ } else { ++ /* Relative basedir: may be wire-influenced via ++ * --link-dest / --copy-dest / --compare-dest. Resolve it ++ * under the same RESOLVE_BENEATH guarantee as relpath, so ++ * a parent symlink on basedir cannot redirect the dirfd ++ * outside the CWD anchor. */ ++ struct open_how bhow; ++ memset(&bhow, 0, sizeof bhow); ++ bhow.flags = O_RDONLY | O_DIRECTORY; ++ bhow.resolve = RESOLVE_BENEATH | RESOLVE_NO_MAGICLINKS; ++ dirfd = syscall(SYS_openat2, AT_FDCWD, basedir, &bhow, sizeof bhow); ++ if (dirfd == -1) ++ return -1; + } + + retfd = syscall(SYS_openat2, dirfd, relpath, &how, sizeof how); +@@ -859,10 +1624,17 @@ static int secure_relative_open_resolve_beneath(const char *basedir, const char + + if (basedir == NULL) { + dirfd = AT_FDCWD; +- } else { ++ } else if (basedir[0] == '/') { ++ /* Absolute basedir: operator-trusted, plain openat. */ + dirfd = openat(AT_FDCWD, basedir, O_RDONLY | O_DIRECTORY); + if (dirfd == -1) + return -1; ++ } else { ++ /* Relative basedir: confine its resolution beneath CWD ++ * (see secure_relative_open_linux for the rationale). */ ++ dirfd = openat(AT_FDCWD, basedir, O_RDONLY | O_DIRECTORY | O_RESOLVE_BENEATH); ++ if (dirfd == -1) ++ return -1; + } + + retfd = openat(dirfd, relpath, flags | O_RESOLVE_BENEATH, mode); +@@ -880,8 +1652,20 @@ int secure_relative_open(const char *basedir, const char *relpath, int flags, mo + errno = EINVAL; + return -1; + } +- if (strncmp(relpath, "../", 3) == 0 || strstr(relpath, "/../")) { +- // no ../ elements allowed in the relpath ++ /* Reject any path with a literal ".." component (bare "..", ++ * "../foo", "foo/..", "foo/../bar", "subdir/.."). The previous ++ * substring-based check caught only "../" prefix and "/../" ++ * substring; bare ".." and trailing "/.." escape on the per- ++ * component walk fallback used by NetBSD/OpenBSD/Solaris/Cygwin ++ * 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; + } +@@ -911,15 +1695,47 @@ int secure_relative_open(const char *basedir, const char *relpath, int flags, mo + #else + int dirfd = AT_FDCWD; + if (basedir != NULL) { +- dirfd = openat(AT_FDCWD, basedir, O_RDONLY | O_DIRECTORY); +- if (dirfd == -1) { +- return -1; ++ if (basedir[0] == '/') { ++ /* Absolute basedir: operator-trusted, plain openat. */ ++ dirfd = openat(AT_FDCWD, basedir, O_RDONLY | O_DIRECTORY); ++ if (dirfd == -1) { ++ return -1; ++ } ++ } else { ++ /* Relative basedir: walk it component-by-component ++ * with O_NOFOLLOW. This is the per-component ++ * RESOLVE_BENEATH equivalent for platforms without ++ * kernel-supported confinement, and matches the ++ * relpath walk below. Symlinks in basedir are ++ * rejected outright on this fallback path; the ++ * Linux openat2 / O_RESOLVE_BENEATH paths above ++ * still allow within-tree symlinks. */ ++ char *bcopy = my_strdup(basedir, __FILE__, __LINE__); ++ if (!bcopy) ++ return -1; ++ for (const char *part = strtok(bcopy, "/"); ++ part != NULL; ++ part = strtok(NULL, "/")) ++ { ++ int next_fd = openat(dirfd, part, O_RDONLY | O_DIRECTORY | O_NOFOLLOW); ++ if (next_fd == -1) { ++ int save_errno = errno; ++ if (dirfd != AT_FDCWD) close(dirfd); ++ free(bcopy); ++ errno = save_errno; ++ return -1; ++ } ++ if (dirfd != AT_FDCWD) close(dirfd); ++ dirfd = next_fd; ++ } ++ free(bcopy); + } + } + int retfd = -1; + + char *path_copy = my_strdup(relpath, __FILE__, __LINE__); + if (!path_copy) { ++ if (dirfd != AT_FDCWD) close(dirfd); + return -1; + } + +@@ -945,8 +1761,15 @@ int secure_relative_open(const char *basedir, const char *relpath, int flags, mo + dirfd = next_fd; + } + +- // the path must be a directory +- errno = EINVAL; ++ /* All components walked as directories. If the caller asked for ++ * O_DIRECTORY, return the dirfd we built up; otherwise the path ++ * resolved to a directory but the caller wanted a regular file. */ ++ if ((flags & O_DIRECTORY) && dirfd != AT_FDCWD) { ++ retfd = dirfd; ++ dirfd = AT_FDCWD; ++ goto cleanup; ++ } ++ errno = EISDIR; + + cleanup: + free(path_copy); +diff --git a/t_secure_relpath.c b/t_secure_relpath.c +new file mode 100644 +index 00000000..a0fdf0d2 +--- /dev/null ++++ b/t_secure_relpath.c +@@ -0,0 +1,151 @@ ++/* ++ * Test harness for secure_relative_open()'s front-door input ++ * validation. Codex audit Finding 5 noted that the existing check ++ * ++ * if (strncmp(relpath, "../", 3) == 0 || strstr(relpath, "/../")) ++ * ++ * catches "../foo" and "foo/../bar" but misses bare ".." (an actual ++ * one-level escape on platforms that fall back to the per-component ++ * walk), as well as "a/..", "foo/..", and any other form that ++ * decomposes to a ".." component when split on "/". The kernel- ++ * enforced RESOLVE_BENEATH (Linux 5.6+) and O_RESOLVE_BENEATH ++ * (FreeBSD 13+, macOS 15+) reject these in-kernel; the per- ++ * component fallback used on NetBSD, OpenBSD, Solaris, Cygwin and ++ * pre-5.6 Linux does not, so the validation must happen at the ++ * front door. ++ * ++ * This helper invokes secure_relative_open() with each suspect ++ * input and checks both the failure (rc < 0) and the errno ++ * (EINVAL means "rejected at the front door"). Pre-fix, the kernel ++ * may reject with a different errno (EXDEV from RESOLVE_BENEATH); ++ * post-fix, the front-door check catches every variant up front ++ * with a consistent EINVAL across platforms. ++ * ++ * Not linked into rsync itself. ++ */ ++ ++#include "rsync.h" ++ ++#include ++ ++int dry_run = 0; ++int am_root = 0; ++int am_sender = 0; ++int read_only = 0; ++int list_only = 0; ++int copy_links = 0; ++int copy_unsafe_links = 0; ++extern int am_daemon, am_chrooted; ++ ++short info_levels[COUNT_INFO], debug_levels[COUNT_DEBUG]; ++ ++static int errs = 0; ++ ++static void check_relpath(const char *relpath) ++{ ++ int fd; ++ int saved_errno; ++ ++ errno = 0; ++ fd = secure_relative_open(NULL, relpath, O_RDONLY | O_DIRECTORY, 0); ++ saved_errno = errno; ++ ++ if (fd >= 0) { ++ fprintf(stderr, ++ "FAIL [relpath=%-12s]: returned valid fd %d (escape) -- expected -1 EINVAL\n", ++ relpath, fd); ++ close(fd); ++ errs++; ++ return; ++ } ++ ++ if (saved_errno != EINVAL) { ++ fprintf(stderr, ++ "FAIL [relpath=%-12s]: rejected but errno=%d (%s), expected EINVAL\n", ++ relpath, saved_errno, strerror(saved_errno)); ++ errs++; ++ return; ++ } ++ ++ fprintf(stderr, "OK [relpath=%-12s]: rejected with EINVAL\n", relpath); ++} ++ ++static void check_basedir(const char *basedir) ++{ ++ int fd; ++ int saved_errno; ++ ++ errno = 0; ++ fd = secure_relative_open(basedir, "ok", O_RDONLY | O_DIRECTORY, 0); ++ saved_errno = errno; ++ ++ if (fd >= 0) { ++ fprintf(stderr, ++ "FAIL [basedir=%-12s]: returned valid fd %d -- expected -1 EINVAL\n", ++ basedir, fd); ++ close(fd); ++ errs++; ++ return; ++ } ++ ++ if (saved_errno != EINVAL) { ++ fprintf(stderr, ++ "FAIL [basedir=%-12s]: rejected but errno=%d (%s), expected EINVAL\n", ++ basedir, saved_errno, strerror(saved_errno)); ++ errs++; ++ return; ++ } ++ ++ fprintf(stderr, "OK [basedir=%-12s]: rejected with EINVAL\n", basedir); ++} ++ ++int main(int argc, char **argv) ++{ ++ if (argc != 2) { ++ fprintf(stderr, "usage: %s \n", argv[0]); ++ return 2; ++ } ++ if (chdir(argv[1]) < 0) { ++ perror("chdir"); ++ return 2; ++ } ++ ++ /* secure_relative_open's daemon-only confinement protections only ++ * fire when am_daemon && !am_chrooted (the threat model is the ++ * daemon-no-chroot deployment), but the front-door input ++ * validation runs unconditionally. We set am_daemon anyway so the ++ * helper exercises the same code shape the receiver does. */ ++ am_daemon = 1; ++ am_chrooted = 0; ++ ++ mkdir("subdir", 0755); ++ ++ /* Each of these relpaths must be rejected with EINVAL at the ++ * secure_relative_open() front door. ".." is the actual one-level ++ * escape; the others ("subdir/..", "subdir/../subdir") resolve ++ * back to the start dir on systems that allow them, but we still ++ * reject them as defence-in-depth: a path containing a ".." token ++ * is suspicious and the caller should normalise before passing ++ * it in. The "../foo" / "foo/../bar" / "/foo" / "/" cases are ++ * regression checks for the existing checks. */ ++ check_relpath(".."); ++ check_relpath("../foo"); ++ check_relpath("subdir/.."); ++ check_relpath("subdir/../subdir"); ++ check_relpath("foo/../bar"); ++ check_relpath("/foo"); ++ check_relpath("/"); ++ ++ /* Same checks against basedir (which the codex Finding 2 fix ++ * routes through the same RESOLVE_BENEATH-equivalent). Absolute ++ * basedirs are operator-trusted and intentionally not validated ++ * here. */ ++ check_basedir(".."); ++ check_basedir("../subdir"); ++ check_basedir("subdir/.."); ++ check_basedir("foo/../bar"); ++ ++ if (errs) ++ fprintf(stderr, "\n%d failure(s)\n", errs); ++ return errs ? 1 : 0; ++} +diff --git a/testsuite/alt-dest-symlink-race.test b/testsuite/alt-dest-symlink-race.test +new file mode 100755 +index 00000000..2256f2f2 +--- /dev/null ++++ b/testsuite/alt-dest-symlink-race.test +@@ -0,0 +1,96 @@ ++#!/bin/sh ++ ++# Copyright (C) 2026 by Andrew Tridgell ++ ++# This program is distributable under the terms of the GNU GPL (see ++# COPYING). ++ ++# Regression test for the basedir-confinement gap in ++# secure_relative_open(). The function opens basedir with a plain ++# openat(AT_FDCWD, basedir, O_RDONLY | O_DIRECTORY), without ++# RESOLVE_BENEATH or a per-component O_NOFOLLOW walk, so a parent ++# symlink ON basedir is followed unrestrictedly. RESOLVE_BENEATH is ++# then applied only to relpath, anchored at the wrong directory. ++# ++# The receiver's basis-file lookup at receiver.c passes ++# basis_dir[fnamecmp_type] (from --copy-dest / --link-dest / ++# --compare-dest -- all sender-controllable in daemon mode) as ++# basedir. A daemon-module attacker with write access can plant a ++# symlink at module/cd -> /outside, then run --link-dest=cd to ++# make the daemon's basis-file lookup resolve into /outside, ++# leaking the contents of daemon-readable files via the rsync ++# delta-rolling read-disclosure primitive. ++# ++# We detect the escape by leveraging --link-dest: when basis ++# matches source exactly (content + mtime + mode), --link-dest ++# hard-links the destination to the basis file. With the bug, the ++# destination ends up as a hard link to the outside-the-module ++# file (same inode). With the fix, no basis is found and the ++# destination is a fresh copy (different inode). ++# ++# The vulnerable code path is the same on every platform ++# (including the per-component fallback on systems without ++# RESOLVE_BENEATH), so this test is not platform-gated. ++ ++. "$suitedir/rsync.fns" ++ ++mod="$scratchdir/module" ++outside="$scratchdir/outside" ++src="$scratchdir/src" ++conf="$scratchdir/test-rsyncd.conf" ++ ++rm -rf "$mod" "$outside" "$src" ++mkdir -p "$mod" "$outside" "$src" ++ ++# Portable inode-number helper (GNU coreutils stat -c, BSD stat -f). ++file_inode() { ++ stat -c %i "$1" 2>/dev/null || stat -f %i "$1" ++} ++ ++# Outside-the-module file an attacker would like the daemon to ++# treat as a basis. ++echo "OUTSIDE_SECRET_DATA" > "$outside/target.txt" ++chmod 0644 "$outside/target.txt" ++ ++# The symlink trap planted in the module by the local attacker. ++ln -s "$outside" "$mod/cd" ++ ++# Source file matches outside/target.txt exactly (content + mtime ++# + mode) so --link-dest will hard-link the destination to the ++# basis file iff the daemon's basedir lookup reaches outside/. ++echo "OUTSIDE_SECRET_DATA" > "$src/target.txt" ++touch -r "$outside/target.txt" "$src/target.txt" ++chmod 0644 "$src/target.txt" ++ ++cat > "$conf" </dev/null 2>&1 || true ++ ++if [ ! -f "$mod/target.txt" ]; then ++ test_fail "destination file was not created -- daemon transfer failed before the test could observe the basedir behaviour" ++fi ++ ++outside_inode=$(file_inode "$outside/target.txt") ++dst_inode=$(file_inode "$mod/target.txt") ++ ++if [ "$outside_inode" = "$dst_inode" ]; then ++ test_fail "basedir-escape: --link-dest hard-linked module/target.txt to outside/target.txt (inode $outside_inode); daemon's basis-file lookup followed the parent symlink on the basedir" ++fi ++ ++exit 0 +diff --git a/testsuite/secure-relpath-validation.test b/testsuite/secure-relpath-validation.test +new file mode 100755 +index 00000000..5b77f7cc +--- /dev/null ++++ b/testsuite/secure-relpath-validation.test +@@ -0,0 +1,34 @@ ++#!/bin/sh ++ ++# Copyright (C) 2026 by Andrew Tridgell ++ ++# This program is distributable under the terms of the GNU GPL (see ++# COPYING). ++ ++# Regression test for codex audit Finding 5: secure_relative_open()'s ++# front-door input check rejects "../foo" and "foo/../bar" but ++# misses bare "..", "subdir/..", and other variants whose "/"-split ++# components contain a literal "..". The kernel-enforced ++# RESOLVE_BENEATH (Linux 5.6+) and O_RESOLVE_BENEATH ++# (FreeBSD 13+, macOS 15+) reject these in-kernel; the per-component ++# walk fallback used on NetBSD, OpenBSD, Solaris, Cygwin and pre-5.6 ++# Linux does not -- so the validation must happen at the front door. ++# ++# This test invokes the t_secure_relpath helper, which calls ++# secure_relative_open() with each suspect input and verifies the ++# return value is -1 with errno == EINVAL. EINVAL is the marker ++# that the front-door rejected the input, not the kernel; pre-fix ++# the kernel returns -1 with EXDEV (or, on the per-component ++# fallback, may return a valid fd at all -- "escape"). ++ ++. "$suitedir/rsync.fns" ++ ++testdir="$scratchdir/relpath-test" ++rm -rf "$testdir" ++mkdir -p "$testdir" ++ ++if ! "$TOOLDIR/t_secure_relpath" "$testdir"; then ++ test_fail "t_secure_relpath rejected one or more inputs incorrectly (see stderr above for the specific case)" ++fi ++ ++exit 0 +diff --git a/util1.c b/util1.c +index 796604f6..f85f33e9 100644 +--- a/util1.c ++++ b/util1.c +@@ -141,7 +141,7 @@ int set_times(const char *fname, STRUCT_STAT *stp) + + #ifdef HAVE_UTIMENSAT + #include "case_N.h" +- if (do_utimensat(fname, stp) == 0) ++ if (do_utimensat_at(fname, stp) == 0) + break; + if (errno != ENOSYS) + return -1; +@@ -479,13 +479,13 @@ int copy_file(const char *source, const char *dest, int tmpfilefd, mode_t mode) + int robust_unlink(const char *fname) + { + #ifndef ETXTBSY +- return do_unlink(fname); ++ return do_unlink_at(fname); + #else + static int counter = 1; + int rc, pos, start; + char path[MAXPATHLEN]; + +- rc = do_unlink(fname); ++ rc = do_unlink_at(fname); + if (rc == 0 || errno != ETXTBSY) + return rc; + +@@ -515,7 +515,7 @@ int robust_unlink(const char *fname) + } + + /* maybe we should return rename()'s exit status? Nah. */ +- if (do_rename(fname, path) != 0) { ++ if (do_rename_at(fname, path) != 0) { + errno = ETXTBSY; + return -1; + } +@@ -538,7 +538,7 @@ int robust_rename(const char *from, const char *to, const char *partialptr, + return 0; + + while (tries--) { +- if (do_rename(from, to) == 0) ++ if (do_rename_at(from, to) == 0) + return 0; + + switch (errno) { +@@ -559,7 +559,7 @@ int robust_rename(const char *from, const char *to, const char *partialptr, + } + if (copy_file(from, to, -1, mode) != 0) + return -2; +- do_unlink(from); ++ do_unlink_at(from); + return 1; + default: + return -1; +@@ -1333,20 +1333,20 @@ int handle_partial_dir(const char *fname, int create) + dir = partial_fname; + if (create) { + STRUCT_STAT st; +- int statret = do_lstat(dir, &st); ++ int statret = do_lstat_at(dir, &st); + if (statret == 0 && !S_ISDIR(st.st_mode)) { +- if (do_unlink(dir) < 0) { ++ if (do_unlink_at(dir) < 0) { + *fn = '/'; + return 0; + } + statret = -1; + } +- if (statret < 0 && do_mkdir(dir, 0700) < 0) { ++ if (statret < 0 && do_mkdir_at(dir, 0700) < 0) { + *fn = '/'; + return 0; + } + } else +- do_rmdir(dir); ++ do_rmdir_at(dir); + *fn = '/'; + + return 1; +diff --git a/xattrs.c b/xattrs.c +index e5d0dd43..5f740bb5 100644 +--- a/xattrs.c ++++ b/xattrs.c +@@ -1249,7 +1249,12 @@ int set_stat_xattr(const char *fname, struct file_struct *file, mode_t new_mode) + + int x_stat(const char *fname, STRUCT_STAT *fst, STRUCT_STAT *xst) + { +- int ret = do_stat(fname, fst); ++ /* Use the *_at variants so that on a daemon-no-chroot deployment ++ * the metadata read goes through a secure parent dirfd instead ++ * of bare path resolution. The *_at wrappers fall through to ++ * plain do_stat outside the daemon-no-chroot context, so this ++ * change is transparent for non-daemon use. */ ++ int ret = do_stat_at(fname, fst); + if ((ret < 0 || get_stat_xattr(fname, -1, fst, xst) < 0) && xst) + xst->st_mode = 0; + return ret; +@@ -1257,7 +1262,7 @@ int x_stat(const char *fname, STRUCT_STAT *fst, STRUCT_STAT *xst) + + int x_lstat(const char *fname, STRUCT_STAT *fst, STRUCT_STAT *xst) + { +- int ret = do_lstat(fname, fst); ++ int ret = do_lstat_at(fname, fst); + if ((ret < 0 || get_stat_xattr(fname, -1, fst, xst) < 0) && xst) + xst->st_mode = 0; + return ret; +-- +2.51.0 + diff -Nru rsync-3.2.7/debian/patches/2026-05-20/0042-util1-syscall-secure-copy_file-source-dest-opens-bar.patch rsync-3.2.7/debian/patches/2026-05-20/0042-util1-syscall-secure-copy_file-source-dest-opens-bar.patch --- rsync-3.2.7/debian/patches/2026-05-20/0042-util1-syscall-secure-copy_file-source-dest-opens-bar.patch 1970-01-01 00:00:00.000000000 +0000 +++ rsync-3.2.7/debian/patches/2026-05-20/0042-util1-syscall-secure-copy_file-source-dest-opens-bar.patch 2026-05-20 05:47:50.000000000 +0000 @@ -0,0 +1,565 @@ +From a277a06b1017b4cf6bb0fe33d5823869ed02dfd9 Mon Sep 17 00:00:00 2001 +From: Andrew Tridgell +Date: Wed, 6 May 2026 09:45:30 +1000 +Subject: [PATCH 42/56] util1+syscall: secure copy_file source/dest opens; + bare-path defence-in-depth + +Three related codex audit findings: + + Finding 3a: copy_file()'s source open in util1.c used + do_open_nofollow(), which only rejects a final-component + symlink. A parent-component symlink (e.g. --copy-dest=cd where + cd -> /outside) follows freely and reads outside the module. + Route through secure_relative_open() with O_NOFOLLOW. + + Finding 3b: generator.c's in-place backup-file create still + used a bare do_open with O_CREAT, leaving a tiny but reachable + parent-symlink window between the secure unlink (already + through do_unlink_at) and the create. Add do_open_at() that + goes through a secure parent dirfd, and route the call site + through it. + + Finding 3c: copy_file()'s destination open in + unlink_and_reopen() had the same bare-do_open pattern; route + through do_open_at as well. + +Adds testsuite/copy-dest-source-symlink.test and +testsuite/bare-do-open-symlink-race.test as regression coverage +for both attack shapes. + +Co-Authored-By: Claude Opus 4.7 (1M context) +--- + generator.c | 2 +- + syscall.c | 138 +++++++++++++++-- + testsuite/bare-do-open-symlink-race.test | 186 +++++++++++++++++++++++ + testsuite/copy-dest-source-symlink.test | 83 ++++++++++ + util1.c | 21 ++- + 5 files changed, 416 insertions(+), 14 deletions(-) + create mode 100755 testsuite/bare-do-open-symlink-race.test + create mode 100755 testsuite/copy-dest-source-symlink.test + +diff --git a/generator.c b/generator.c +index a6bce20b..b80eb2e3 100644 +--- a/generator.c ++++ b/generator.c +@@ -1896,7 +1896,7 @@ static void recv_generator(char *fname, struct file_struct *file, int ndx, + back_file = NULL; + goto cleanup; + } +- if ((f_copy = do_open(backupptr, O_WRONLY | O_CREAT | O_TRUNC | O_EXCL, 0600)) < 0) { ++ if ((f_copy = do_open_at(backupptr, O_WRONLY | O_CREAT | O_TRUNC | O_EXCL, 0600)) < 0) { + rsyserr(FERROR_XFER, errno, "open %s", full_fname(backupptr)); + unmake_file(back_file); + back_file = NULL; +diff --git a/syscall.c b/syscall.c +index 2cff0b38..47777350 100644 +--- a/syscall.c ++++ b/syscall.c +@@ -203,11 +203,6 @@ int do_symlink_at(const char *lnk, const char *path) + if (!am_daemon || am_chrooted) + return do_symlink(lnk, path); + +-#if defined NO_SYMLINK_XATTRS || defined NO_SYMLINK_USER_XATTRS +- if (am_root < 0) +- return do_symlink(lnk, path); +-#endif +- + if (!path || !*path || *path == '/') + return do_symlink(lnk, path); + +@@ -228,6 +223,34 @@ int do_symlink_at(const char *lnk, const char *path) + if (dfd < 0) + return -1; + ++#if defined NO_SYMLINK_XATTRS || defined NO_SYMLINK_USER_XATTRS ++ /* For --fake-super, do_symlink writes the link target into a ++ * regular file rather than creating a real symlink. Do that ++ * here against the secure dirfd, with O_NOFOLLOW so a pre- ++ * planted symlink at the basename can't redirect the file ++ * creation. (Previously the fake-super branch fell through to ++ * the bare-path do_symlink at the top of the function.) */ ++ if (am_root < 0) { ++ int len = strlen(lnk); ++ int fd = openat(dfd, bname, ++ O_WRONLY | O_CREAT | O_TRUNC | O_NOFOLLOW, ++ S_IWUSR | S_IRUSR); ++ if (fd < 0) { ++ e = errno; ++ close(dfd); ++ errno = e; ++ return -1; ++ } ++ ret = (write(fd, lnk, len) == len) ? 0 : -1; ++ if (close(fd) < 0) ++ ret = -1; ++ e = errno; ++ close(dfd); ++ errno = e; ++ return ret; ++ } ++#endif ++ + ret = symlinkat(lnk, dfd, bname); + e = errno; + close(dfd); +@@ -503,9 +526,12 @@ int do_mknod(const char *pathname, mode_t mode, dev_t dev) + mknodat() against that dirfd. mknodat() covers both regular-file + (S_IFREG with dev=0) and FIFO (S_IFIFO) and device-node creation. + +- Falls through to do_mknod() for fake-super (am_root < 0) and for +- sockets, both of which use auxiliary path-based syscalls that +- don't have an *at() variant in any portable form. ++ Fake-super (am_root < 0) is handled inline against the secure ++ parent dirfd: it creates a regular empty file (the same file-as- ++ metadata-placeholder pattern do_mknod uses) via openat() with ++ O_NOFOLLOW. Sockets fall through to do_mknod() because their ++ bind(2) takes a path argument with no portable bindat() variant; ++ this is documented as a residual. + */ + int do_mknod_at(const char *pathname, mode_t mode, dev_t dev) + { +@@ -523,9 +549,6 @@ int do_mknod_at(const char *pathname, mode_t mode, dev_t dev) + if (!am_daemon || am_chrooted) + return do_mknod(pathname, mode, dev); + +- if (am_root < 0) +- return do_mknod(pathname, mode, dev); +- + #if !defined MKNOD_CREATES_SOCKETS && defined HAVE_SYS_UN_H + if (S_ISSOCK(mode)) + return do_mknod(pathname, mode, dev); +@@ -551,6 +574,29 @@ int do_mknod_at(const char *pathname, mode_t mode, dev_t dev) + if (dfd < 0) + return -1; + ++ if (am_root < 0) { ++ /* For --fake-super, do_mknod creates a regular empty ++ * file as a placeholder for the special-file metadata ++ * (which is stored in xattrs elsewhere). Do that against ++ * the secure dirfd, with O_NOFOLLOW so a pre-planted ++ * symlink at the basename can't redirect the file ++ * creation. */ ++ int fd = openat(dfd, bname, ++ O_WRONLY | O_CREAT | O_TRUNC | O_NOFOLLOW, ++ S_IWUSR | S_IRUSR); ++ if (fd < 0) { ++ e = errno; ++ close(dfd); ++ errno = e; ++ return -1; ++ } ++ ret = (close(fd) < 0) ? -1 : 0; ++ e = errno; ++ close(dfd); ++ errno = e; ++ return ret; ++ } ++ + #if !defined MKNOD_CREATES_FIFOS && defined HAVE_MKFIFO + if (S_ISFIFO(mode)) + ret = mkfifoat(dfd, bname, mode); +@@ -639,6 +685,76 @@ int do_open(const char *pathname, int flags, mode_t mode) + return open(pathname, flags | O_BINARY, mode); + } + ++/* ++ Symlink-race-safe variant of do_open() for receiver-side use. See ++ the comment on do_chmod_at() for the threat model. open() resolves ++ parent components, so a parent-symlink swap can redirect the open ++ to a file outside the module. This wrapper is defence-in-depth for ++ bare-path do_open() sites that callers know are otherwise ++ protected by secure parent-syscalls (e.g. generator.c's in-place ++ backup creation, where robust_unlink() rejects the symlinked ++ parent before this open is reached): if any of those upstream ++ protections is later removed or regresses, the open here still ++ refuses to escape the module. ++ ++ Defence: open the parent of pathname under secure_relative_open() ++ and call openat() against the resulting dirfd with O_NOFOLLOW ++ (so the basename itself isn't followed if it happens to be a ++ pre-planted symlink, which is what we want for O_CREAT|O_EXCL). ++*/ ++int do_open_at(const char *pathname, int flags, mode_t mode) ++{ ++#ifdef AT_FDCWD ++ extern int am_daemon, am_chrooted; ++ char dirpath[MAXPATHLEN]; ++ const char *bname; ++ const char *slash; ++ int dfd, ret, e; ++ size_t dlen; ++ ++ if (flags != O_RDONLY) { ++ RETURN_ERROR_IF(dry_run, 0); ++ RETURN_ERROR_IF_RO_OR_LO; ++ } ++ ++ if (!am_daemon || am_chrooted) ++ return do_open(pathname, flags, mode); ++ ++ if (!pathname || !*pathname || *pathname == '/') ++ return do_open(pathname, flags, mode); ++ ++ slash = strrchr(pathname, '/'); ++ if (!slash) ++ return do_open(pathname, flags, mode); ++ ++ dlen = slash - pathname; ++ if (dlen >= sizeof dirpath) { ++ errno = ENAMETOOLONG; ++ return -1; ++ } ++ memcpy(dirpath, pathname, dlen); ++ dirpath[dlen] = '\0'; ++ bname = slash + 1; ++ ++ dfd = secure_relative_open(NULL, dirpath, O_RDONLY | O_DIRECTORY, 0); ++ if (dfd < 0) ++ return -1; ++ ++#ifdef O_NOATIME ++ if (open_noatime) ++ flags |= O_NOATIME; ++#endif ++ ++ ret = openat(dfd, bname, flags | O_NOFOLLOW | O_BINARY, mode); ++ e = errno; ++ close(dfd); ++ errno = e; ++ return ret; ++#else ++ return do_open(pathname, flags, mode); ++#endif ++} ++ + #ifdef HAVE_CHMOD + int do_chmod(const char *path, mode_t mode) + { +diff --git a/testsuite/bare-do-open-symlink-race.test b/testsuite/bare-do-open-symlink-race.test +new file mode 100755 +index 00000000..b8c51bbe +--- /dev/null ++++ b/testsuite/bare-do-open-symlink-race.test +@@ -0,0 +1,186 @@ ++#!/bin/sh ++ ++# Copyright (C) 2026 by Andrew Tridgell ++ ++# This program is distributable under the terms of the GNU GPL (see ++# COPYING). ++ ++# Regression test for codex audit Findings 3b and 3c: ++# ++# 3b: generator.c:1905 -- the in-place backup creation opens ++# backupptr via bare do_open(O_WRONLY|O_CREAT|O_TRUNC|O_EXCL). ++# With --backup-dir set to an attacker-planted parent symlink, ++# the backup file is written outside the module under the ++# daemon's authority. ++# ++# 3c-symlink: syscall.c:207 -- do_symlink_at falls through to bare ++# do_symlink for am_root < 0 (fake-super), which then opens ++# the destination path with bare open() (final-component ++# fake-super file). A parent symlink on the destination path ++# redirects the file creation outside the module. ++# ++# 3c-mknod: syscall.c:506 -- do_mknod_at falls through to bare ++# do_mknod for am_root < 0, same path-based open(). For ++# FIFOs/sockets/devices the bare path is also used. ++# ++# Each scenario plants a "secret" file outside the module at a ++# location the symlink trap points to. The check is that the ++# outside file's content and mode are unchanged after the attack ++# attempt. ++ ++. "$suitedir/rsync.fns" ++ ++# All three scenarios depend on receiver-side daemon code paths ++# that are only secured on platforms with a working ++# secure_relative_open. The chdir/chmod tests already skip the ++# same set; mirror that. ++case "$(uname -s)" in ++ SunOS|OpenBSD|NetBSD|CYGWIN*) ++ test_skipped "secure_relative_open relies on RESOLVE_BENEATH-equivalent kernel support not available on $(uname -s)" ++ ;; ++esac ++ ++mod="$scratchdir/module" ++outside="$scratchdir/outside" ++src="$scratchdir/src" ++conf="$scratchdir/test-rsyncd.conf" ++ ++# Portable inode-and-mode helpers. ++file_mode() { ++ stat -c %a "$1" 2>/dev/null || stat -f %Lp "$1" ++} ++ ++setup() { ++ rm -rf "$mod" "$outside" "$src" ++ mkdir -p "$mod" "$outside" "$src" ++ ++ echo "OUTSIDE_PROTECTED_DATA" > "$outside/target.txt" ++ chmod 0644 "$outside/target.txt" ++ outside_pristine="$scratchdir/outside-pristine.txt" ++ cp -p "$outside/target.txt" "$outside_pristine" ++ ++ ln -s "$outside" "$mod/cd" ++} ++ ++verify_outside_unchanged() { ++ label="$1" ++ mode=$(file_mode "$outside/target.txt") ++ case "$mode" in ++ 644|0644) ;; ++ *) test_fail "$label: outside/target.txt mode changed from 644 to $mode" ;; ++ esac ++ if ! cmp -s "$outside/target.txt" "$outside_pristine"; then ++ test_fail "$label: outside/target.txt content changed -- daemon followed the cd symlink" ++ fi ++} ++ ++verify_outside_unchanged_or_absent() { ++ label="$1" ++ target="$2" # specific file under outside/ to check absence of ++ if [ -e "$outside/$target" ]; then ++ test_fail "$label: outside/$target was created -- daemon followed the cd symlink" ++ fi ++} ++ ++ ++############################################################ ++# Scenario 3b: --inplace --backup --backup-dir=cd ++# ++# Pre-create module/target.txt so the receiver enters the in-place ++# update path; a backup of the existing content must be made ++# before the update. With --backup-dir=cd, backupptr resolves to ++# "cd/target.txt"; with the bug, robust_unlink and the bare ++# do_open at generator.c:1905 both follow the cd symlink, the ++# unlink deletes outside/target.txt and the create writes the ++# pre-existing module/target.txt content there. ++############################################################ ++ ++setup ++echo "EXISTING_MODULE_DATA" > "$mod/target.txt" ++chmod 0666 "$mod/target.txt" ++echo "NEW_DATA_FROM_SENDER" > "$src/target.txt" ++chmod 0644 "$src/target.txt" ++ ++cat > "$conf" </dev/null 2>&1 || true ++ ++verify_outside_unchanged "3b inplace+backup-dir=cd" ++ ++ ++############################################################ ++# Scenario 3c-symlink: fake-super symlink push to a path with a ++# symlinked parent ++# ++# With "fake super = yes" set on the module, the receiver ++# represents symlinks as fake-super files (regular files with the ++# link target written to them). The path-based open() in ++# do_symlink's fake-super branch follows parent symlinks. We push ++# a single symlink to the destination path "cd/sym" so the ++# receiver's create-file call lands at "cd/sym" relative to the ++# module root, where cd is the symlink trap. ++############################################################ ++ ++setup ++ ++mkdir -p "$src/cd" ++ln -s /etc/passwd "$src/cd/sym" ++ ++cat > "$conf" </dev/null 2>&1 || true ++ ++verify_outside_unchanged_or_absent "3c-symlink fake-super symlink push" "sym" ++ ++ ++############################################################ ++# Scenario 3c-mknod: fake-super FIFO push to a path with a ++# symlinked parent ++# ++# Similar to 3c-symlink but for special files. mkfifo works ++# without root; we push a FIFO and verify the receiver doesn't ++# create a fake-super file at outside/fifo. ++############################################################ ++ ++setup ++ ++mkdir -p "$src/cd" ++mkfifo "$src/cd/fifo" 2>/dev/null ++if [ ! -p "$src/cd/fifo" ]; then ++ test_skipped "mkfifo unavailable; cannot exercise 3c-mknod" ++fi ++ ++cat > "$conf" </dev/null 2>&1 || true ++ ++verify_outside_unchanged_or_absent "3c-mknod fake-super FIFO push" "fifo" ++ ++exit 0 +diff --git a/testsuite/copy-dest-source-symlink.test b/testsuite/copy-dest-source-symlink.test +new file mode 100755 +index 00000000..2d20fab4 +--- /dev/null ++++ b/testsuite/copy-dest-source-symlink.test +@@ -0,0 +1,83 @@ ++#!/bin/sh ++ ++# Copyright (C) 2026 by Andrew Tridgell ++ ++# This program is distributable under the terms of the GNU GPL (see ++# COPYING). ++ ++# Regression test for codex audit Finding 3a: copy_file()'s source ++# open in copy_altdest_file() is via do_open_nofollow(), which only ++# refuses a final-component symlink. Parent components are still ++# resolved with normal symlink-following. A daemon module attacker ++# who plants a parent symlink at module/cd -> /outside, then runs ++# --copy-dest=cd against a source file matching the size+mtime of ++# /outside/target.txt, drives the receiver to: ++# ++# 1. Find a match-level >= 2 basis at "cd/target.txt" ++# 2. Call copy_altdest_file -> copy_file(src="cd/target.txt", ...) ++# 3. do_open_nofollow follows the "cd" parent symlink and reads ++# the contents of /outside/target.txt under the daemon's ++# authority ++# 4. Copy that content into the module destination ++# ++# Result: outside/target.txt content lands at module/target.txt, ++# accessible to the attacker on a subsequent pull. ++# ++# We detect by content: src/target.txt and outside/target.txt have ++# identical metadata (size + mtime + mode) but different content. ++# After the transfer, module/target.txt should match src (no ++# basedir escape) -- if it matches outside, the bug copied across ++# the symlink boundary. ++ ++. "$suitedir/rsync.fns" ++ ++mod="$scratchdir/module" ++outside="$scratchdir/outside" ++src="$scratchdir/src" ++conf="$scratchdir/test-rsyncd.conf" ++ ++rm -rf "$mod" "$outside" "$src" ++mkdir -p "$mod" "$outside" "$src" ++ ++# Outside-the-module file the daemon should not read on the ++# attacker's behalf. ++echo "OUTSIDE_LEAKED_DATA!" > "$outside/target.txt" ++chmod 0644 "$outside/target.txt" ++ ++# The symlink trap. ++ln -s "$outside" "$mod/cd" ++ ++# Source: same size, same mtime, same mode as outside -- so the ++# generator's link_stat + quick_check_ok finds a match-level >= 2 ++# basis and calls copy_altdest_file. ++echo "ATTACKER_KNOWN_DATA!" > "$src/target.txt" ++touch -r "$outside/target.txt" "$src/target.txt" ++chmod 0644 "$src/target.txt" ++ ++cat > "$conf" </dev/null 2>&1 || true ++ ++if [ ! -f "$mod/target.txt" ]; then ++ test_fail "destination file was not created -- daemon transfer failed before the test could observe the basedir behaviour" ++fi ++ ++if cmp -s "$mod/target.txt" "$outside/target.txt"; then ++ test_fail "basedir-escape via copy_file source: module/target.txt now contains the contents of outside/target.txt -- daemon read /outside via the cd symlink and copied it into the module" ++fi ++ ++if ! cmp -s "$mod/target.txt" "$src/target.txt"; then ++ test_fail "destination doesn't match source content (and isn't outside content either): unexpected state" ++fi ++ ++exit 0 +diff --git a/util1.c b/util1.c +index f85f33e9..49ead492 100644 +--- a/util1.c ++++ b/util1.c +@@ -336,7 +336,13 @@ static int unlink_and_reopen(const char *dest, mode_t mode) + mode |= S_IWUSR; + #endif + mode &= INITACCESSPERMS; +- if ((ofd = do_open(dest, O_WRONLY | O_CREAT | O_TRUNC | O_EXCL, mode)) < 0) { ++ /* Use do_open_at so the create/truncate goes through a secure ++ * parent dirfd in the daemon-no-chroot deployment. Otherwise ++ * an attacker could swap a parent component with a symlink in ++ * the window between robust_unlink (which uses do_unlink_at, ++ * already secure) and the create here, and redirect the new ++ * file outside the module. */ ++ if ((ofd = do_open_at(dest, O_WRONLY | O_CREAT | O_TRUNC | O_EXCL, mode)) < 0) { + int save_errno = errno; + rsyserr(FERROR_XFER, save_errno, "open %s", full_fname(dest)); + errno = save_errno; +@@ -360,12 +366,23 @@ static int unlink_and_reopen(const char *dest, mode_t mode) + * --copy-dest options. */ + int copy_file(const char *source, const char *dest, int tmpfilefd, mode_t mode) + { ++ extern int am_daemon, am_chrooted; + int ifd, ofd; + char buf[1024 * 8]; + int len; /* Number of bytes read into `buf'. */ + OFF_T prealloc_len = 0, offset = 0; + +- if ((ifd = do_open_nofollow(source, O_RDONLY)) < 0) { ++ /* On a daemon without chroot, route the source open through ++ * secure_relative_open so a parent-symlink on the source path ++ * (e.g. --copy-dest=cd where cd is a symlink to an outside ++ * directory) cannot redirect the read to a file the daemon can ++ * see but the attacker should not. Plain do_open_nofollow only ++ * refuses a final-component symlink; parents are still followed. */ ++ if (am_daemon && !am_chrooted && source && *source && source[0] != '/') ++ ifd = secure_relative_open(NULL, source, O_RDONLY | O_NOFOLLOW, 0); ++ else ++ ifd = do_open_nofollow(source, O_RDONLY); ++ if (ifd < 0) { + int save_errno = errno; + rsyserr(FERROR_XFER, errno, "open %s", full_fname(source)); + errno = save_errno; +-- +2.51.0 + diff -Nru rsync-3.2.7/debian/patches/2026-05-20/0043-testsuite-end-to-end-regression-test-for-chdir-symli.patch rsync-3.2.7/debian/patches/2026-05-20/0043-testsuite-end-to-end-regression-test-for-chdir-symli.patch --- rsync-3.2.7/debian/patches/2026-05-20/0043-testsuite-end-to-end-regression-test-for-chdir-symli.patch 1970-01-01 00:00:00.000000000 +0000 +++ rsync-3.2.7/debian/patches/2026-05-20/0043-testsuite-end-to-end-regression-test-for-chdir-symli.patch 2026-05-20 05:47:50.000000000 +0000 @@ -0,0 +1,289 @@ +From 7c8a647c2ead898f283457f2f240c311b222c8e1 Mon Sep 17 00:00:00 2001 +From: Andrew Tridgell +Date: Tue, 5 May 2026 14:34:50 +1000 +Subject: [PATCH 43/56] testsuite: end-to-end regression test for + chdir-symlink-race + +testsuite/chdir-symlink-race.test runs an actual rsync daemon +(via RSYNC_CONNECT_PROG to avoid the network) configured with +"use chroot = no", plants a symlink at module/subdir -> ../outside, +and runs four flavours of attacker-shaped transfer (single-file +poc_chmod, -r push into the symlinked subdir with --size-only and +without, -r push into the module root). All four must leave the +outside-the-module sentinel file's mode AND content unchanged. + +Portability: + - file_mode() helper falls back to BSD stat -f %Lp when GNU + stat -c %a is unavailable (macOS, FreeBSD). + - Pre-saved pristine copy + cmp(1) replaces sha1sum, which + differs across platforms (sha1sum / shasum / sha1). + +Tests are kept running as root in the user-namespace re-exec +wrapper used by symlink-race tests so the daemon's setuid path +doesn't drop into the test user's identity (which on Linux +would mean the chmod-escape code path can't trigger because +the test user doesn't have CAP_FOWNER over the outside file). + +Co-Authored-By: Claude Opus 4.7 (1M context) +--- + testsuite/alt-dest-symlink-race.test | 17 +++ + testsuite/bare-do-open-symlink-race.test | 20 ++++ + testsuite/chdir-symlink-race.test | 135 +++++++++++++++++++++++ + testsuite/copy-dest-source-symlink.test | 15 +++ + 4 files changed, 187 insertions(+) + create mode 100755 testsuite/chdir-symlink-race.test + +diff --git a/testsuite/alt-dest-symlink-race.test b/testsuite/alt-dest-symlink-race.test +index 2256f2f2..fd36c6e6 100755 +--- a/testsuite/alt-dest-symlink-race.test ++++ b/testsuite/alt-dest-symlink-race.test +@@ -62,8 +62,25 @@ echo "OUTSIDE_SECRET_DATA" > "$src/target.txt" + touch -r "$outside/target.txt" "$src/target.txt" + chmod 0644 "$src/target.txt" + ++# When running as root the daemon would drop to "nobody" by ++# default, which can't write into the test scratch dir. Force the ++# daemon to keep our uid/gid in that case so the basis-link ++# transfer can actually create the destination file. (Non-root ++# can't specify uid/gid in rsyncd.conf -- comment them out then.) ++my_uid=`get_testuid` ++root_uid=`get_rootuid` ++root_gid=`get_rootgid` ++uid_setting="uid = $root_uid" ++gid_setting="gid = $root_gid" ++if test x"$my_uid" != x"$root_uid"; then ++ uid_setting="#$uid_setting" ++ gid_setting="#$gid_setting" ++fi ++ + cat > "$conf" < "$conf" < "$conf" < "$conf" < ../outside, and runs four flavours of ++# rsync transfer that previously all reached files in ../outside: ++# ++# 1. single-file dest = subdir/target.txt (the original poc_chmod) ++# 2. -r src/subdir/ to upload/subdir/ (the chdir-escape case) ++# 3. -r src/subdir/ to upload/subdir/ (no --size-only: forces basis read+write) ++# 4. -r src/ to upload/ (was already protected by the ++# original CVE-2026-29518 fix; ++# regression-checked here) ++# ++# All four must leave the outside-the-module sentinel file's mode AND ++# content unchanged. ++ ++. "$suitedir/rsync.fns" ++ ++case "$(uname -s)" in ++ SunOS|OpenBSD|NetBSD|CYGWIN*) ++ test_skipped "secure chdir relies on RESOLVE_BENEATH-equivalent kernel support not available on $(uname -s)" ++ ;; ++esac ++ ++mod="$scratchdir/module" ++outside="$scratchdir/outside" ++src="$scratchdir/src" ++conf="$scratchdir/test-rsyncd.conf" ++ ++rm -rf "$mod" "$outside" "$src" ++mkdir -p "$mod" "$outside" "$src" "$src/subdir" ++ ++# Portable octal-mode helper -- macOS and FreeBSD's stat use -f, GNU ++# coreutils stat uses -c. ++file_mode() { ++ stat -c %a "$1" 2>/dev/null || stat -f %Lp "$1" ++} ++ ++# The "secret" file outside the module the attacker is trying to alter. ++# Save a pristine copy alongside it so we can compare with cmp(1) rather ++# than depending on sha1sum/shasum/sha1, which differ across platforms. ++echo "OUTSIDE_SECRET_DATA" > "$outside/target.txt" ++chmod 0600 "$outside/target.txt" ++outside_pristine="$scratchdir/outside-pristine.txt" ++cp -p "$outside/target.txt" "$outside_pristine" ++ ++# Symlink trap planted in the module by the local attacker. ++ln -s "$outside" "$mod/subdir" ++ ++# Source files the sender will push: same size as the outside target, ++# different content, mode 0666 (the perms the attacker tries to push). ++SIZE=$(stat -c %s "$outside/target.txt" 2>/dev/null \ ++ || stat -f %z "$outside/target.txt") ++head -c "$SIZE" /dev/urandom > "$src/target.txt" ++head -c "$SIZE" /dev/urandom > "$src/subdir/target.txt" ++chmod 0666 "$src/target.txt" "$src/subdir/target.txt" ++ ++cat > "$conf" < "$outside/target.txt" ++} ++ ++verify_unchanged() { ++ label="$1" ++ mode=$(file_mode "$outside/target.txt") ++ case "$mode" in ++ 600|0600) ;; ++ *) test_fail "$label: outside file mode changed from 600 to $mode (chmod escape)" ;; ++ esac ++ if ! cmp -s "$outside/target.txt" "$outside_pristine"; then ++ test_fail "$label: outside file content changed (write escape)" ++ fi ++} ++ ++run_attack() { ++ label="$1"; shift ++ reset_outside ++ RSYNC_CONNECT_PROG="$RSYNC --config=$conf --daemon" \ ++ $RSYNC "$@" >/dev/null 2>&1 || true ++ verify_unchanged "$label" ++} ++ ++# 1. The original poc_chmod scenario: single file, dest path with ++# the symlinked subdir as a path component. With --size-only the ++# receiver normally skips the basis open and goes straight to chmod ++# -- only the chdir-escape blocks the chmod from reaching outside. ++run_attack "single-file --size-only" \ ++ -tp --size-only \ ++ "$src/target.txt" rsync://localhost/upload/subdir/target.txt ++ ++# 2. -r push into the symlinked subdir: receiver chdir's into "subdir", ++# follows the symlink, ends up in outside. ++run_attack "-r --size-only into subdir/" \ ++ -rtp --size-only \ ++ "$src/subdir/" rsync://localhost/upload/subdir/ ++ ++# 3. Same but no --size-only -- forces the basis-file open and a real ++# rename, so this exercises the read-disclosure and write-escape ++# paths together. ++run_attack "-r without --size-only into subdir/" \ ++ -rtp \ ++ "$src/subdir/" rsync://localhost/upload/subdir/ ++ ++# 4. -r src/ to upload/ -- this case was already covered by the ++# original CVE-2026-29518 fix because the receiver stays at module ++# root and operates on slashed paths. Regression check. ++run_attack "-r --size-only into upload/ root" \ ++ -rtp --size-only \ ++ "$src/" rsync://localhost/upload/ ++ ++exit 0 +diff --git a/testsuite/copy-dest-source-symlink.test b/testsuite/copy-dest-source-symlink.test +index 2d20fab4..f91ee986 100755 +--- a/testsuite/copy-dest-source-symlink.test ++++ b/testsuite/copy-dest-source-symlink.test +@@ -54,8 +54,23 @@ echo "ATTACKER_KNOWN_DATA!" > "$src/target.txt" + touch -r "$outside/target.txt" "$src/target.txt" + chmod 0644 "$src/target.txt" + ++# When running as root the daemon would drop to "nobody" by ++# default and fail to mkstemp in the scratch dir; force it to ++# keep our uid/gid in that case. ++my_uid=`get_testuid` ++root_uid=`get_rootuid` ++root_gid=`get_rootgid` ++uid_setting="uid = $root_uid" ++gid_setting="gid = $root_gid" ++if test x"$my_uid" != x"$root_uid"; then ++ uid_setting="#$uid_setting" ++ gid_setting="#$gid_setting" ++fi ++ + cat > "$conf" < +Date: Wed, 29 Apr 2026 11:10:59 +1000 +Subject: [PATCH 44/56] 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) +--- + receiver.c | 11 ++++- + token.c | 117 ++++++++++++++++++++++++++--------------------------- + 2 files changed, 67 insertions(+), 61 deletions(-) + +diff --git a/receiver.c b/receiver.c +index 8f5b51dd..63e5cedb 100644 +--- a/receiver.c ++++ b/receiver.c +@@ -318,7 +318,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); + +@@ -326,6 +331,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 b7a02ea1..02dabd8d 100644 +--- a/token.c ++++ b/token.c +@@ -291,6 +291,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; + } + +@@ -493,9 +501,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) + { +@@ -586,22 +637,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 (rx_token < 0) { +- rprintf(FERROR, "invalid token number in compressed stream\n"); +- exit_cleanup(RERR_PROTOCOL); +- } +- } +- 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; +@@ -621,10 +657,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(); + } + } + } +@@ -833,22 +866,7 @@ static int32 recv_zstd_token(int f, char **data) + return 0; + } + /* here we have a token of some kind */ +- if (flag & TOKEN_REL) { +- rx_token += flag & 0x3f; +- flag >>= 6; +- } else { +- rx_token = read_int(f); +- if (rx_token < 0) { +- rprintf(FERROR, "invalid token number in compressed stream\n"); +- exit_cleanup(RERR_PROTOCOL); +- } +- } +- if (flag & 1) { +- rx_run = read_byte(f); +- rx_run += read_byte(f) << 8; +- recv_state = r_running; +- } +- return -1 - rx_token; ++ return recv_compressed_token_num(f, flag); + + case r_inflated: /* zstd doesn't get into this state */ + break; +@@ -879,10 +897,7 @@ static int32 recv_zstd_token(int f, char **data) + break; + + case r_running: +- ++rx_token; +- if (--rx_run == 0) +- recv_state = r_idle; +- return -1 - rx_token; ++ return recv_compressed_token_run(); + } + } + } +@@ -1002,22 +1017,7 @@ static int32 recv_compressed_token(int f, char **data) + } + + /* here we have a token of some kind */ +- if (flag & TOKEN_REL) { +- rx_token += flag & 0x3f; +- flag >>= 6; +- } else { +- rx_token = read_int(f); +- if (rx_token < 0) { +- rprintf(FERROR, "invalid token number in compressed stream\n"); +- exit_cleanup(RERR_PROTOCOL); +- } +- } +- if (flag & 1) { +- rx_run = read_byte(f); +- rx_run += read_byte(f) << 8; +- recv_state = r_running; +- } +- return -1 - rx_token; ++ return recv_compressed_token_num(f, flag); + + case r_inflating: + avail_out = LZ4_decompress_safe(next_in, dbuf, avail_in, size); +@@ -1033,10 +1033,7 @@ static int32 recv_compressed_token(int f, char **data) + break; + + case r_running: +- ++rx_token; +- if (--rx_run == 0) +- recv_state = r_idle; +- return -1 - rx_token; ++ return recv_compressed_token_run(); + } + } + } +-- +2.51.0 + diff -Nru rsync-3.2.7/debian/patches/2026-05-20/0045-testsuite-cover-refuse-options-compress-for-the-daem.patch rsync-3.2.7/debian/patches/2026-05-20/0045-testsuite-cover-refuse-options-compress-for-the-daem.patch --- rsync-3.2.7/debian/patches/2026-05-20/0045-testsuite-cover-refuse-options-compress-for-the-daem.patch 1970-01-01 00:00:00.000000000 +0000 +++ rsync-3.2.7/debian/patches/2026-05-20/0045-testsuite-cover-refuse-options-compress-for-the-daem.patch 2026-05-20 05:47:50.000000000 +0000 @@ -0,0 +1,81 @@ +From 4255413e0c1adc763caf65f44ee4336a985fbd57 Mon Sep 17 00:00:00 2001 +From: Andrew Tridgell +Date: Fri, 1 May 2026 10:56:17 +1000 +Subject: [PATCH 45/56] testsuite: cover 'refuse options = compress' for the + daemon + +Add a daemon-refuse-compress test that builds a module configured with +'refuse options = compress' and asserts that: + 1. an attempted -z transfer to that module fails with an error + mentioning --compress, and + 2. the same transfer without -z still succeeds. + +This pins down the documented way to disable all compression on a +daemon, which previously had no automated coverage. + +Co-Authored-By: Claude Opus 4.7 (1M context) +--- + testsuite/daemon-refuse-compress.test | 51 +++++++++++++++++++++++++++ + 1 file changed, 51 insertions(+) + create mode 100644 testsuite/daemon-refuse-compress.test + +diff --git a/testsuite/daemon-refuse-compress.test b/testsuite/daemon-refuse-compress.test +new file mode 100644 +index 00000000..a24e50d1 +--- /dev/null ++++ b/testsuite/daemon-refuse-compress.test +@@ -0,0 +1,51 @@ ++#!/bin/sh ++ ++# Copyright (C) 2026 by Andrew Tridgell ++ ++# This program is distributable under the terms of the GNU GPL (see ++# COPYING). ++ ++# Test that a daemon module configured with "refuse options = compress" ++# rejects clients that ask for compression and still serves the same ++# transfer when the client does not. ++ ++. "$suitedir/rsync.fns" ++ ++build_rsyncd_conf ++ ++# Append a module that refuses --compress (-z). ++cat >>"$conf" </dev/null 2>"$errlog"; then ++ cat "$errlog" >&2 ++ test_fail "compressed transfer was not refused" ++fi ++ ++grep -- '--compress' "$errlog" >/dev/null || { ++ cat "$errlog" >&2 ++ test_fail "expected refuse error mentioning --compress" ++} ++ ++# The same transfer without -z must succeed. ++rm -rf "$todir" ++mkdir "$todir" ++checkit "$RSYNC -av localhost::no-compress/ '$todir/'" "$chkdir" "$todir" ++ ++# The script would have aborted on error, so getting here means we've won. ++exit 0 +-- +2.51.0 + diff -Nru rsync-3.2.7/debian/patches/2026-05-20/0046-receiver-add-parent_ndx-0-guard-mirroring-797e17f.patch rsync-3.2.7/debian/patches/2026-05-20/0046-receiver-add-parent_ndx-0-guard-mirroring-797e17f.patch --- rsync-3.2.7/debian/patches/2026-05-20/0046-receiver-add-parent_ndx-0-guard-mirroring-797e17f.patch 1970-01-01 00:00:00.000000000 +0000 +++ rsync-3.2.7/debian/patches/2026-05-20/0046-receiver-add-parent_ndx-0-guard-mirroring-797e17f.patch 2026-05-20 05:47:50.000000000 +0000 @@ -0,0 +1,119 @@ +From 0a5fa00fdcbacbebb89daca0ae68ae320f22dc74 Mon Sep 17 00:00:00 2001 +From: Andrew Tridgell +Date: Tue, 5 May 2026 16:48:16 +1000 +Subject: [PATCH 46/56] 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) +--- + 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.51.0 + diff -Nru rsync-3.2.7/debian/patches/2026-05-20/0047-clientserver-fix-hostname-ACL-bypass-when-using-daem.patch rsync-3.2.7/debian/patches/2026-05-20/0047-clientserver-fix-hostname-ACL-bypass-when-using-daem.patch --- rsync-3.2.7/debian/patches/2026-05-20/0047-clientserver-fix-hostname-ACL-bypass-when-using-daem.patch 1970-01-01 00:00:00.000000000 +0000 +++ rsync-3.2.7/debian/patches/2026-05-20/0047-clientserver-fix-hostname-ACL-bypass-when-using-daem.patch 2026-05-20 05:47:50.000000000 +0000 @@ -0,0 +1,192 @@ +From 74ea276900779b95ddd1769d1d6ae78b2fd1a790 Mon Sep 17 00:00:00 2001 +From: Andrew Tridgell +Date: Wed, 31 Dec 2025 13:50:35 +1100 +Subject: [PATCH 47/56] clientserver: fix hostname ACL bypass when using daemon + chroot + +On an rsync daemon configured with "daemon chroot", the reverse-DNS +lookup of the connecting client was performed *after* the chroot +had been entered. If the chroot did not contain the files glibc +needs for resolution (/etc/resolv.conf, /etc/nsswitch.conf, +/etc/hosts, NSS service modules), the lookup failed and +client_name() returned "UNKNOWN". Hostname-based deny rules +("hosts deny = *.evil.example") therefore could not match, and +an attacker controlling their PTR record could connect from a +hostname the administrator had intended to deny. IP-based ACLs +were unaffected. + +Do the reverse DNS lookup before chroot/setuid; client_name() +caches its result, so the post-chroot call uses the cached value +and hostname-based ACLs work even when DNS is unavailable +post-chroot. + +Adds testsuite/daemon-chroot-acl.test as end-to-end regression +coverage. The test sets up an empty chroot directory, configures +"hosts deny = " with daemon chroot, and +asserts the connection is refused with @ERROR access denied. +Uses unshare --user --map-root-user for non-root CAP_SYS_CHROOT; +skips cleanly on non-Linux or when user namespaces aren't +available. + +Reporter: Joshua Rogers (MegaManSec). + +Co-Authored-By: Claude Opus 4.7 (1M context) +--- + clientserver.c | 22 ++++++ + testsuite/daemon-chroot-acl.test | 111 +++++++++++++++++++++++++++++++ + 2 files changed, 133 insertions(+) + create mode 100644 testsuite/daemon-chroot-acl.test + +diff --git a/clientserver.c b/clientserver.c +index e8dfddb1..14daba3c 100644 +--- a/clientserver.c ++++ b/clientserver.c +@@ -1312,6 +1312,28 @@ int start_daemon(int f_in, int f_out) + if (lp_proxy_protocol() && !read_proxy_protocol_header(f_in)) + return -1; + ++ /* Do reverse DNS lookup before chroot/setuid. The result is cached, ++ * so the later client_name() call will use this cached value. This ++ * ensures hostname-based ACLs work even when DNS is unavailable ++ * after chroot. ++ * ++ * "reverse lookup" can be set globally OR per-module, so we also ++ * scan each module: a deployment with "reverse lookup = no" in the ++ * global section but "reverse lookup = yes" in a specific module ++ * still triggers a post-chroot lookup at access-check time ++ * (rsync_module() in this file), which would also fail in the ++ * chroot and turn hostname-based deny rules into silent bypasses. */ ++ { ++ int need_reverse = lp_reverse_lookup(-1); ++ int j, num_modules = lp_num_modules(); ++ for (j = 0; !need_reverse && j < num_modules; j++) { ++ if (lp_reverse_lookup(j)) ++ need_reverse = 1; ++ } ++ if (need_reverse) ++ (void)client_name(client_addr(f_in)); ++ } ++ + p = lp_daemon_chroot(); + if (*p) { + log_init(0); /* Make use we've initialized syslog before chrooting. */ +diff --git a/testsuite/daemon-chroot-acl.test b/testsuite/daemon-chroot-acl.test +new file mode 100644 +index 00000000..9d1c1b63 +--- /dev/null ++++ b/testsuite/daemon-chroot-acl.test +@@ -0,0 +1,111 @@ ++#!/bin/sh ++ ++# Copyright (C) 2026 by Andrew Tridgell ++ ++# This program is distributable under the terms of the GNU GPL (see ++# COPYING). ++ ++# Regression test for GHSA-rjfm-3w2m-jf4f: a hostname-based "hosts deny" ++# rule must still match when the daemon performs a 'daemon chroot' and ++# the chroot does not contain the NSS files glibc needs for reverse DNS. ++# ++# Pre-fix, reverse DNS happened *after* the daemon chroot. With an empty ++# chroot the NSS lookup failed, client_name() returned "UNKNOWN", and a ++# deny rule referring to the connecting hostname silently failed to ++# match. ++# ++# Two scenarios are exercised so we can distinguish the case the fix ++# definitely covers from the per-module path that may still be ++# vulnerable: ++# A. global "reverse lookup = yes" (covered by b6abdb4c) ++# B. only module "reverse lookup = yes" (gap to verify) ++ ++. "$suitedir/rsync.fns" ++ ++case `uname -s` in ++Linux*) ;; ++*) test_skipped "test is Linux-specific (uses chroot+unshare)" ;; ++esac ++ ++# We need CAP_SYS_CHROOT. Re-exec under a user namespace if not root. ++if ! chroot / /bin/true 2>/dev/null; then ++ if [ -z "$RSYNC_UNSHARED" ] && unshare --user --map-root-user true 2>/dev/null; then ++ echo "Re-running under unshare --user --map-root-user..." ++ RSYNC_UNSHARED=1 exec unshare --user --map-root-user "$SHELL_PATH" $RUNSHFLAGS "$0" ++ fi ++ test_skipped "need CAP_SYS_CHROOT (root or unshare --user --map-root-user)" ++fi ++ ++# We need 127.0.0.1 to reverse-resolve to a real hostname while NSS is ++# still working (i.e. before the daemon's chroot). The daemon will ++# look that name up itself as part of its hostname-based ACL check; ++# we then deny that name and assert the connection is rejected. ++client_hostname=`getent hosts 127.0.0.1 2>/dev/null | awk 'NR==1 {print $2}'` ++if [ -z "$client_hostname" ] || [ "$client_hostname" = "127.0.0.1" ]; then ++ test_skipped "no reverse DNS for 127.0.0.1" ++fi ++ ++chrootdir="$scratchdir/chroot" ++rm -rf "$chrootdir" ++mkdir -p "$chrootdir/modroot" ++echo "from chroot" > "$chrootdir/modroot/file1" ++ ++conf="$scratchdir/test-rsyncd.conf" ++logfile="$scratchdir/rsyncd.log" ++ ++write_conf() { ++ cat >"$conf" <"$out" 2>&1 ++ rc=$? ++ ++ echo "----- $label (rsync exit $rc):" ++ cat "$out" ++ echo "----- daemon log:" ++ [ -f "$logfile" ] && cat "$logfile" ++ echo "-----" ++ ++ grep -q '@ERROR.*access denied' "$out" ++} ++ ++# Scenario A: global reverse lookup. Covered by b6abdb4c. ++write_conf yes yes ++if ! run_check "Scenario A (global reverse lookup = yes)"; then ++ test_fail "Scenario A: hostname deny rule was bypassed" ++fi ++ ++# Scenario B: only the per-module reverse-lookup setting is enabled. ++# The b6abdb4c fix only pre-warms client_name()'s cache when the ++# global setting is on, so the post-chroot lookup in this path may ++# still produce "UNKNOWN" and bypass the deny rule. ++write_conf no yes ++if ! run_check "Scenario B (per-module reverse lookup only)"; then ++ test_fail "Scenario B: hostname deny rule was bypassed (per-module reverse lookup with daemon chroot still has the bypass)" ++fi ++ ++exit 0 +-- +2.51.0 + diff -Nru rsync-3.2.7/debian/patches/2026-05-20/0048-defence-in-depth-bound-wire-supplied-counts-and-leng.patch rsync-3.2.7/debian/patches/2026-05-20/0048-defence-in-depth-bound-wire-supplied-counts-and-leng.patch --- rsync-3.2.7/debian/patches/2026-05-20/0048-defence-in-depth-bound-wire-supplied-counts-and-leng.patch 1970-01-01 00:00:00.000000000 +0000 +++ rsync-3.2.7/debian/patches/2026-05-20/0048-defence-in-depth-bound-wire-supplied-counts-and-leng.patch 2026-05-20 05:47:50.000000000 +0000 @@ -0,0 +1,261 @@ +From f50c1d59c4eff3c8c868aa3b0b79fe56e6949eb8 Mon Sep 17 00:00:00 2001 +From: Andrew Tridgell +Date: Wed, 31 Dec 2025 12:56:54 +1100 +Subject: [PATCH 48/56] defence-in-depth: bound wire-supplied counts and + lengths + +Multiple receiver-side fields read from the wire were trusted +without upper-bound checks. A hostile peer could either request +extreme allocations (DoS via --max-alloc) or, on platforms where +read_varint returned a negative value, push ~SIZE_MAX through the +size_t conversion to wrap downstream length checks. + +Introduce read_int_bounded(), read_varint_bounded() and +read_varint_size() in io.c so wire-derived integer ranges are +checked at the read site rather than scattered across each +caller, with RERR_PROTOCOL on out-of-range input. + +Apply the bounded primitives to: + - sum->count (checksum count -- previously could overflow + (size_t)count * xfer_sum_len on 32-bit with raised max-alloc) + - xattrs: count, name_len, datum_len, plus rel_pos overflow + detect to stop chain wrapping the num accumulator + - acls: ida-entry count + - flist: file mode S_IFMT validation, modtime_nsec range check + - delete-stat counters in main: per-summand cap so the total + can't overflow a signed 32-bit accumulator + +Reporters include Joshua Rogers (checksum-count overflow finding). + +Co-Authored-By: Claude Opus 4.7 (1M context) +--- + acls.c | 2 +- + flist.c | 17 ++++++++++++++--- + io.c | 54 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ + main.c | 10 +++++----- + rsync.h | 17 +++++++++++++++++ + xattrs.c | 16 ++++++++++++---- + 6 files changed, 103 insertions(+), 13 deletions(-) + +diff --git a/acls.c b/acls.c +index cd6aadcd..8eff1555 100644 +--- a/acls.c ++++ b/acls.c +@@ -697,7 +697,7 @@ static uint32 recv_acl_access(int f, uchar *name_follows_ptr) + static uchar recv_ida_entries(int f, ida_entries *ent) + { + uchar computed_mask_bits = 0; +- int i, count = read_varint(f); ++ int i, count = read_varint_bounded(f, 0, MAX_WIRE_ACL_COUNT, "ACL count"); + + ent->idas = count ? new_array(id_access, count) : NULL; + ent->count = count; +diff --git a/flist.c b/flist.c +index 3b801e3f..51e76feb 100644 +--- a/flist.c ++++ b/flist.c +@@ -840,9 +840,9 @@ static struct file_struct *recv_file_entry(int f, struct file_list *flist, int x + } + if (xflags & XMIT_MOD_NSEC) + #ifndef CAN_SET_NSEC +- (void)read_varint(f); ++ (void)read_varint_bounded(f, 0, MAX_WIRE_NSEC, "modtime_nsec"); + #else +- modtime_nsec = read_varint(f); ++ modtime_nsec = read_varint_bounded(f, 0, MAX_WIRE_NSEC, "modtime_nsec"); + else + modtime_nsec = 0; + #endif +@@ -861,8 +861,19 @@ static struct file_struct *recv_file_entry(int f, struct file_list *flist, int x + #endif + } + #endif +- if (!(xflags & XMIT_SAME_MODE)) ++ if (!(xflags & XMIT_SAME_MODE)) { + mode = from_wire_mode(read_int(f)); ++ /* Reject modes whose type bits are not one of the standard ++ * file types; otherwise garbage mode values propagate through ++ * the file-type checks below unpredictably. */ ++ if (!S_ISREG(mode) && !S_ISDIR(mode) && !S_ISLNK(mode) ++ && !S_ISCHR(mode) && !S_ISBLK(mode) ++ && !S_ISFIFO(mode) && !S_ISSOCK(mode)) { ++ rprintf(FERROR, "invalid file mode 0%o for %s [%s]\n", ++ (unsigned)mode, lastname, who_am_i()); ++ exit_cleanup(RERR_PROTOCOL); ++ } ++ } + if (atimes_ndx && !S_ISDIR(mode) && !(xflags & XMIT_SAME_ATIME)) { + atime = read_varlong(f, 4); + #if SIZEOF_TIME_T < SIZEOF_INT64 +diff --git a/io.c b/io.c +index 2d94c1f4..eb316383 100644 +--- a/io.c ++++ b/io.c +@@ -1868,6 +1868,45 @@ int64 read_varlong(int f, uchar min_bytes) + return u.x; + } + ++/* Read an int32 and verify lo <= v <= hi. On out-of-range, abort with a ++ * protocol error naming "what". The bound is co-located with the read so it ++ * cannot be forgotten by a downstream user. */ ++int32 read_int_bounded(int f, int32 lo, int32 hi, const char *what) ++{ ++ int32 v = read_int(f); ++ if (v < lo || v > hi) { ++ rprintf(FERROR, "wire value %s out of range: %ld not in [%ld,%ld] [%s]\n", ++ what, (long)v, (long)lo, (long)hi, who_am_i()); ++ exit_cleanup(RERR_PROTOCOL); ++ } ++ return v; ++} ++ ++/* As read_int_bounded but for varint-encoded values. */ ++int32 read_varint_bounded(int f, int32 lo, int32 hi, const char *what) ++{ ++ int32 v = read_varint(f); ++ if (v < lo || v > hi) { ++ rprintf(FERROR, "wire value %s out of range: %ld not in [%ld,%ld] [%s]\n", ++ what, (long)v, (long)lo, (long)hi, who_am_i()); ++ exit_cleanup(RERR_PROTOCOL); ++ } ++ return v; ++} ++ ++/* Read a varint that will be used as a size_t. Rejects negative values ++ * (which would wrap to ~SIZE_MAX) and values exceeding the supplied max. */ ++size_t read_varint_size(int f, size_t max, const char *what) ++{ ++ int32 v = read_varint(f); ++ if (v < 0 || (size_t)v > max) { ++ rprintf(FERROR, "wire size %s out of range: %ld > %lu [%s]\n", ++ what, (long)v, (unsigned long)max, who_am_i()); ++ exit_cleanup(RERR_PROTOCOL); ++ } ++ return (size_t)v; ++} ++ + int64 read_longint(int f) + { + #if SIZEOF_INT64 >= 8 +@@ -1974,6 +2013,21 @@ void read_sum_head(int f, struct sum_struct *sum) + (long)sum->count, who_am_i()); + exit_cleanup(RERR_PROTOCOL); + } ++ /* Guard against integer overflow in downstream allocations sized by ++ * count*element_size. my_alloc uses divide-not-multiply so it is ++ * already wraparound-safe, but checking here gives a clearer error ++ * and also covers the (size_t)count * xfer_sum_len arithmetic that ++ * is performed *before* reaching my_alloc. */ ++ if (xfer_sum_len > 0 && (size_t)sum->count > SIZE_MAX / (size_t)xfer_sum_len) { ++ rprintf(FERROR, "Invalid checksum count %ld (too large) [%s]\n", ++ (long)sum->count, who_am_i()); ++ exit_cleanup(RERR_PROTOCOL); ++ } ++ if ((size_t)sum->count > SIZE_MAX / sizeof(struct sum_buf)) { ++ rprintf(FERROR, "Invalid checksum count %ld (sum_buf overflow) [%s]\n", ++ (long)sum->count, who_am_i()); ++ exit_cleanup(RERR_PROTOCOL); ++ } + sum->blength = read_int(f); + if (sum->blength < 0 || sum->blength > max_blength) { + rprintf(FERROR, "Invalid block length %ld [%s]\n", +diff --git a/main.c b/main.c +index d2a7b9b5..1e234efa 100644 +--- a/main.c ++++ b/main.c +@@ -237,11 +237,11 @@ void write_del_stats(int f) + + void read_del_stats(int f) + { +- stats.deleted_files = read_varint(f); +- stats.deleted_files += stats.deleted_dirs = read_varint(f); +- stats.deleted_files += stats.deleted_symlinks = read_varint(f); +- stats.deleted_files += stats.deleted_devices = read_varint(f); +- stats.deleted_files += stats.deleted_specials = read_varint(f); ++ stats.deleted_files = read_varint_bounded(f, 0, MAX_WIRE_DEL_STAT, "deleted_files"); ++ stats.deleted_files += stats.deleted_dirs = read_varint_bounded(f, 0, MAX_WIRE_DEL_STAT, "deleted_dirs"); ++ stats.deleted_files += stats.deleted_symlinks = read_varint_bounded(f, 0, MAX_WIRE_DEL_STAT, "deleted_symlinks"); ++ stats.deleted_files += stats.deleted_devices = read_varint_bounded(f, 0, MAX_WIRE_DEL_STAT, "deleted_devices"); ++ stats.deleted_files += stats.deleted_specials = read_varint_bounded(f, 0, MAX_WIRE_DEL_STAT, "deleted_specials"); + } + + static void become_copy_as_user() +diff --git a/rsync.h b/rsync.h +index b9a7101a..c419a9d6 100644 +--- a/rsync.h ++++ b/rsync.h +@@ -163,6 +163,23 @@ + /* For compatibility with older rsyncs */ + #define OLD_MAX_BLOCK_SIZE ((int32)1 << 29) + ++/* Policy ceilings on attacker-controlled wire values. Picked well above any ++ * legitimate filesystem / protocol traffic but well below sizes that could ++ * cause integer overflow or DoS-grade allocations. See input_checking.txt. ++ * ++ * Note on MAX_WIRE_XATTR_DATALEN: xattr datum size is bounded only by the ++ * wire-format maximum (signed int32 varint, ~2GB). macOS resource forks ++ * are transferred as the com.apple.ResourceFork xattr and can legitimately ++ * be many GB; --max-alloc (default 1GB, configurable) is the real ++ * allocation cap. read_varint_size() still rejects negative values so a ++ * hostile peer cannot wrap to ~SIZE_MAX. */ ++#define MAX_WIRE_XATTR_COUNT 65536 ++#define MAX_WIRE_XATTR_NAMELEN 4096 ++#define MAX_WIRE_XATTR_DATALEN ((int32)0x7fffffff) ++#define MAX_WIRE_ACL_COUNT 65536 ++#define MAX_WIRE_NSEC 999999999 ++#define MAX_WIRE_DEL_STAT ((int32)1 << 30) ++ + #define ROUND_UP_1024(siz) ((siz) & (1024-1) ? ((siz) | (1024-1)) + 1 : (siz)) + + #define IOERR_GENERAL (1<<0) /* For backward compatibility, this must == 1 */ +diff --git a/xattrs.c b/xattrs.c +index 5f740bb5..99795f24 100644 +--- a/xattrs.c ++++ b/xattrs.c +@@ -697,6 +697,13 @@ int recv_xattr_request(struct file_struct *file, int f_in) + rxa = lst->items; + num = 0; + while ((rel_pos = read_varint(f_in)) != 0) { ++ /* Detect signed overflow before the accumulating add. A hostile ++ * peer could otherwise wrap 'num' to land on an arbitrary value. */ ++ if ((rel_pos > 0 && num > INT_MAX - rel_pos) ++ || (rel_pos < 0 && num < INT_MIN - rel_pos)) { ++ rprintf(FERROR, "xattr rel_pos accumulation overflow [%s]\n", who_am_i()); ++ exit_cleanup(RERR_PROTOCOL); ++ } + num += rel_pos; + if (am_sender) { + /* The sender-related num values are only in order on the sender. +@@ -742,7 +749,7 @@ int recv_xattr_request(struct file_struct *file, int f_in) + } + + old_datum = rxa->datum; +- rxa->datum_len = read_varint(f_in); ++ rxa->datum_len = read_varint_size(f_in, MAX_WIRE_XATTR_DATALEN, "xattr datum_len"); + + if (SIZE_MAX - rxa->name_len < rxa->datum_len) + overflow_exit("recv_xattr_request"); +@@ -783,7 +790,8 @@ void receive_xattr(int f, struct file_struct *file) + return; + } + +- if ((count = read_varint(f)) != 0) { ++ count = read_varint_bounded(f, 0, MAX_WIRE_XATTR_COUNT, "xattr count"); ++ if (count != 0) { + (void)EXPAND_ITEM_LIST(&temp_xattr, rsync_xa, count); + temp_xattr.count = 0; + } +@@ -791,8 +799,8 @@ void receive_xattr(int f, struct file_struct *file) + for (num = 1; num <= count; num++) { + char *ptr, *name; + rsync_xa *rxa; +- size_t name_len = read_varint(f); +- size_t datum_len = read_varint(f); ++ size_t name_len = read_varint_size(f, MAX_WIRE_XATTR_NAMELEN, "xattr name_len"); ++ size_t datum_len = read_varint_size(f, MAX_WIRE_XATTR_DATALEN, "xattr datum_len"); + size_t dget_len = datum_len > MAX_FULL_DATUM ? 1 + (size_t)xattr_sum_len : datum_len; + size_t extra_len = MIGHT_NEED_RPRE ? RPRE_LEN : 0; + if (SIZE_MAX - dget_len < extra_len || SIZE_MAX - dget_len - extra_len < name_len) +-- +2.51.0 + diff -Nru rsync-3.2.7/debian/patches/2026-05-20/0049-defence-in-depth-guard-cumulative-snprintf-against-l.patch rsync-3.2.7/debian/patches/2026-05-20/0049-defence-in-depth-guard-cumulative-snprintf-against-l.patch --- rsync-3.2.7/debian/patches/2026-05-20/0049-defence-in-depth-guard-cumulative-snprintf-against-l.patch 1970-01-01 00:00:00.000000000 +0000 +++ rsync-3.2.7/debian/patches/2026-05-20/0049-defence-in-depth-guard-cumulative-snprintf-against-l.patch 2026-05-20 05:47:50.000000000 +0000 @@ -0,0 +1,79 @@ +From 816aa3205968386996cbc353dd35cde66978977d Mon Sep 17 00:00:00 2001 +From: Andrew Tridgell +Date: Fri, 1 May 2026 09:30:31 +1000 +Subject: [PATCH 49/56] defence-in-depth: guard cumulative snprintf against + length underflow + +Two cumulative-snprintf patterns in log.c (rsyserr) and main.c +(output_itemized_counts) had the shape + + len = snprintf(buf, sizeof buf, ...); + len += snprintf(buf+len, sizeof buf - len, ...); + +with no guard between calls. snprintf returns the would-have-been +length on truncation, so a truncated first call leaves +"sizeof buf - len" as a negative-then-promoted-to-size_t value, +underflowing into a huge size_t and writing past buf. + +Realistic exposure is small in both cases (log header well under +buffer, only ~5 itemized iterations writing ~25 chars each into a +1024-byte buffer) but the defect class matches bb0a8118 and the +fix is cheap. Guard before each subsequent call. + +Co-Authored-By: Claude Opus 4.7 (1M context) +--- + log.c | 12 +++++++++--- + main.c | 9 +++++++++ + 2 files changed, 18 insertions(+), 3 deletions(-) + +diff --git a/log.c b/log.c +index e4ba1cce..b948f16a 100644 +--- a/log.c ++++ b/log.c +@@ -456,11 +456,17 @@ void rsyserr(enum logcode code, int errcode, const char *format, ...) + char buf[BIGPATHBUFLEN]; + size_t len; + ++ /* snprintf returns the would-have-been length on truncation, so ++ * each cumulative call must be guarded; if not, sizeof buf - len ++ * can underflow when promoted to size_t and the next call writes ++ * past the buffer. */ + len = snprintf(buf, sizeof buf, RSYNC_NAME ": [%s] ", who_am_i()); + +- va_start(ap, format); +- len += vsnprintf(buf + len, sizeof buf - len, format, ap); +- va_end(ap); ++ if (len < sizeof buf) { ++ va_start(ap, format); ++ len += vsnprintf(buf + len, sizeof buf - len, format, ap); ++ va_end(ap); ++ } + + if (len < sizeof buf) { + len += snprintf(buf + len, sizeof buf - len, +diff --git a/main.c b/main.c +index 1e234efa..f26f256f 100644 +--- a/main.c ++++ b/main.c +@@ -392,9 +392,18 @@ static void output_itemized_counts(const char *prefix, int *counts) + counts[0] -= counts[1] + counts[2] + counts[3] + counts[4]; + for (j = 0; j < 5; j++) { + if (counts[j]) { ++ /* snprintf can return more than its size arg ++ * on truncation; keep len <= sizeof buf - 2 so ++ * the closing ')' and trailing NUL always ++ * have room and the next iteration's ++ * sizeof buf - len - 2 cannot underflow. */ ++ if (len >= (int)sizeof buf - 2) ++ break; + len += snprintf(buf+len, sizeof buf - len - 2, + "%s%s: %s", + pre, labels[j], comma_num(counts[j])); ++ if (len > (int)sizeof buf - 2) ++ len = (int)sizeof buf - 2; + pre = ", "; + } + } +-- +2.51.0 + diff -Nru rsync-3.2.7/debian/patches/2026-05-20/0050-defence-in-depth-receiver-block-index-bounds-read_de.patch rsync-3.2.7/debian/patches/2026-05-20/0050-defence-in-depth-receiver-block-index-bounds-read_de.patch --- rsync-3.2.7/debian/patches/2026-05-20/0050-defence-in-depth-receiver-block-index-bounds-read_de.patch 1970-01-01 00:00:00.000000000 +0000 +++ rsync-3.2.7/debian/patches/2026-05-20/0050-defence-in-depth-receiver-block-index-bounds-read_de.patch 2026-05-20 05:47:50.000000000 +0000 @@ -0,0 +1,80 @@ +From 3ac8349e07e13975f7b8cd3cac777de37b4e8fdb Mon Sep 17 00:00:00 2001 +From: Andrew Tridgell +Date: Wed, 31 Dec 2025 14:01:34 +1100 +Subject: [PATCH 50/56] defence-in-depth: receiver block-index bounds + + read_delay_line null check + +Two assorted audit findings: + + - receive_data() never bounds-checked the block index returned + by recv_token() against sum.count before computing offset2 + and feeding it to map_ptr(). An out-of-bounds index from a + hostile sender produces invalid memory access. Add a + sum.count bounds check. + + - read_delay_line()'s strchr() call could return NULL when no + space was found, but the code unconditionally added 1 to the + result before dereferencing. Low impact (just a disconnect on + exit of the client-specific forked process) but the NULL + deref is real. Guard the NULL. + +Both reported by Joshua Rogers. + +Co-Authored-By: Claude Opus 4.7 (1M context) +--- + generator.c | 14 ++++++++++---- + receiver.c | 5 +++++ + 2 files changed, 15 insertions(+), 4 deletions(-) + +diff --git a/generator.c b/generator.c +index 38f5ad33..e5c9f741 100644 +--- a/generator.c ++++ b/generator.c +@@ -229,11 +229,13 @@ static int read_delay_line(char *buf, int *flags_p) + *flags_p = 0; + + if (sscanf(bp, "%x ", &mode) != 1) { +- invalid_data: +- rprintf(FERROR, "ERROR: invalid data in delete-delay file.\n"); +- return -1; ++ goto invalid_data; ++ } ++ past_space = strchr(bp, ' '); ++ if (!past_space) { ++ goto invalid_data; + } +- past_space = strchr(bp, ' ') + 1; ++ past_space++; + len = j - read_pos - (past_space - bp) + 1; /* count the '\0' */ + read_pos = j + 1; + +@@ -247,6 +249,10 @@ static int read_delay_line(char *buf, int *flags_p) + memcpy(buf, past_space, len); + + return mode; ++ ++invalid_data: ++ rprintf(FERROR, "ERROR: invalid data in delete-delay file.\n"); ++ return -1; + } + + static void do_delayed_deletions(char *delbuf) +diff --git a/receiver.c b/receiver.c +index 0a993e0f..89d2515b 100644 +--- a/receiver.c ++++ b/receiver.c +@@ -352,6 +352,11 @@ static int receive_data(int f_in, char *fname_r, int fd_r, OFF_T size_r, + } + + i = -(i+1); ++ if (i < 0 || i >= sum.count) { ++ rprintf(FERROR, "Invalid block index %d (count=%ld) [%s]\n", ++ i, (long)sum.count, who_am_i()); ++ exit_cleanup(RERR_PROTOCOL); ++ } + offset2 = i * (OFF_T)sum.blength; + len = sum.blength; + if (i == (int)sum.count-1 && sum.remainder != 0) +-- +2.51.0 + diff -Nru rsync-3.2.7/debian/patches/2026-05-20/0052-exclude-fix-crashes-with-fortified-strlcpy.patch rsync-3.2.7/debian/patches/2026-05-20/0052-exclude-fix-crashes-with-fortified-strlcpy.patch --- rsync-3.2.7/debian/patches/2026-05-20/0052-exclude-fix-crashes-with-fortified-strlcpy.patch 1970-01-01 00:00:00.000000000 +0000 +++ rsync-3.2.7/debian/patches/2026-05-20/0052-exclude-fix-crashes-with-fortified-strlcpy.patch 2026-05-20 05:47:50.000000000 +0000 @@ -0,0 +1,51 @@ +From 9354f27f9d91f212b14779101af1eba8a1067a34 Mon Sep 17 00:00:00 2001 +From: Jiri Slaby +Date: Fri, 18 Aug 2023 08:26:20 +0200 +Subject: [PATCH 52/56] exclude: fix crashes with fortified strlcpy() + +Fortified (-D_FORTIFY_SOURCE=2 for gcc) builds make strlcpy() crash when +its third parameter (size) is larger than the buffer: + $ rsync -FFXHav '--filter=merge global-rsync-filter' Align-37-43/ xxx + sending incremental file list + *** buffer overflow detected ***: terminated + +It's in the exclude code in setup_merge_file(): + strlcpy(y, save, MAXPATHLEN); + +Note the 'y' pointer was incremented, so it no longer points to memory +with MAXPATHLEN "owned" bytes. + +Fix it by remembering the number of copied bytes into the 'save' buffer +and use that instead of MAXPATHLEN which is clearly incorrect. + +Fixes #511. +--- + exclude.c | 5 +++-- + 1 file changed, 3 insertions(+), 2 deletions(-) + +diff --git a/exclude.c b/exclude.c +index 684253e5..c7e87e83 100644 +--- a/exclude.c ++++ b/exclude.c +@@ -720,7 +720,8 @@ static BOOL setup_merge_file(int mergelist_num, filter_rule *ex, + parent_dirscan = True; + while (*y) { + char save[MAXPATHLEN]; +- strlcpy(save, y, MAXPATHLEN); ++ /* copylen is strlen(y) which is < MAXPATHLEN. +1 for \0 */ ++ size_t copylen = strlcpy(save, y, MAXPATHLEN) + 1; + *y = '\0'; + dirbuf_len = y - dirbuf; + strlcpy(x, ex->pattern, MAXPATHLEN - (x - buf)); +@@ -734,7 +735,7 @@ static BOOL setup_merge_file(int mergelist_num, filter_rule *ex, + lp->head = NULL; + } + lp->tail = NULL; +- strlcpy(y, save, MAXPATHLEN); ++ strlcpy(y, save, copylen); + while ((*x++ = *y++) != '/') {} + } + parent_dirscan = False; +-- +2.51.0 + diff -Nru rsync-3.2.7/debian/patches/2026-05-20/0053-testsuite-use-integer-sleep-in-clean-fname-underflow.patch rsync-3.2.7/debian/patches/2026-05-20/0053-testsuite-use-integer-sleep-in-clean-fname-underflow.patch --- rsync-3.2.7/debian/patches/2026-05-20/0053-testsuite-use-integer-sleep-in-clean-fname-underflow.patch 1970-01-01 00:00:00.000000000 +0000 +++ rsync-3.2.7/debian/patches/2026-05-20/0053-testsuite-use-integer-sleep-in-clean-fname-underflow.patch 2026-05-20 05:47:50.000000000 +0000 @@ -0,0 +1,32 @@ +From 189612f94aa2bddb5036dddeb3144c28071b95f5 Mon Sep 17 00:00:00 2001 +From: Andrew Tridgell +Date: Thu, 7 May 2026 08:06:28 +1000 +Subject: [PATCH 53/56] testsuite: use integer sleep in + clean-fname-underflow.test + +Solaris /usr/bin/sleep is POSIX and rejects fractional seconds, which +made the test abort silently under `set -eu` (empty log, FAIL). One +second is more than enough for the daemon to listen. + +Co-Authored-By: Claude Opus 4.7 (1M context) +--- + testsuite/clean-fname-underflow.test | 3 ++- + 1 file changed, 2 insertions(+), 1 deletion(-) + +diff --git a/testsuite/clean-fname-underflow.test b/testsuite/clean-fname-underflow.test +index 56d4fece..262a4f07 100644 +--- a/testsuite/clean-fname-underflow.test ++++ b/testsuite/clean-fname-underflow.test +@@ -40,7 +40,8 @@ PORT=$(awk 'BEGIN{srand(); printf "%d", 20000+int(rand()*20000)}') + "$RSYNC_BIN" --daemon --no-detach --config=rsyncd.conf --port="$PORT" >/dev/null 2>&1 & + DAEMON_PID=$! + # Give the daemon a moment to come up. +-sleep 0.3 ++# Use integer second; subsecond sleep is not portable (e.g. Solaris /usr/bin/sleep). ++sleep 1 + + # Invoke the server-side path. We don't need a real transfer; we just want to + # ensure clean_fname() doesn't crash when given "a/../test" via --filter=merge. +-- +2.51.0 + diff -Nru rsync-3.2.7/debian/patches/2026-05-20/0055-popt-fix-poptDupArgv-strlcpy-size-argument.patch rsync-3.2.7/debian/patches/2026-05-20/0055-popt-fix-poptDupArgv-strlcpy-size-argument.patch --- rsync-3.2.7/debian/patches/2026-05-20/0055-popt-fix-poptDupArgv-strlcpy-size-argument.patch 1970-01-01 00:00:00.000000000 +0000 +++ rsync-3.2.7/debian/patches/2026-05-20/0055-popt-fix-poptDupArgv-strlcpy-size-argument.patch 2026-05-20 05:47:50.000000000 +0000 @@ -0,0 +1,55 @@ +From f406c780ec57072caf0c8c9eb928089409be659d Mon Sep 17 00:00:00 2001 +From: Andrew Tridgell +Date: Thu, 7 May 2026 08:55:36 +1000 +Subject: [PATCH 55/56] popt: fix poptDupArgv strlcpy size argument +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +The bundled popt 1.18 (rsync 3.2.7) calls strlcpy(dst, argv[i], nb) +inside the per-arg loop in poptDupArgv(), where nb is the TOTAL +allocation size — not the remaining bytes after dst has advanced. +The actual write was always within the malloc'd buffer, so it was +silent on older glibcs, but glibc 2.39+ fortified strlcpy compares +the size argument against __bos(dst) and aborts with "*** buffer +overflow detected ***" once dst passes through any bytes. + +That broke ~15 tests on Ubuntu 24.04 / glibc 2.39 in CI (any test +spawning a child rsync via popt's argv duplication path). Pass the +remaining bytes (end_buf - dst) so the size argument matches reality. + +Master fixed the same bug differently in popt 1.19 (4c8683c8 "update +to popt 1.19") by switching to stpcpy, but pulling that 1500-line +refresh into a security backport is heavier than warranted. + +Co-Authored-By: Claude Opus 4.7 (1M context) +--- + popt/poptparse.c | 12 +++++++++--- + 1 file changed, 9 insertions(+), 3 deletions(-) + +diff --git a/popt/poptparse.c b/popt/poptparse.c +index e003a04a..06e39f5d 100644 +--- a/popt/poptparse.c ++++ b/popt/poptparse.c +@@ -36,9 +36,15 @@ int poptDupArgv(int argc, const char **argv, + dst += (argc + 1) * sizeof(*argv); + + /*@-branchstate@*/ +- for (i = 0; i < argc; i++) { +- argv2[i] = dst; +- dst += strlcpy(dst, argv[i], nb) + 1; ++ { ++ char * const end_buf = (char *)argv2 + nb; ++ for (i = 0; i < argc; i++) { ++ argv2[i] = dst; ++ /* nb is the TOTAL buffer size, not the remaining bytes; use the ++ * remaining bytes from dst to end_buf so glibc 2.39+ fortified ++ * strlcpy doesn't trip __bos() and abort. */ ++ dst += strlcpy(dst, argv[i], end_buf - dst) + 1; ++ } + } + /*@=branchstate@*/ + argv2[argc] = NULL; +-- +2.51.0 + diff -Nru rsync-3.2.7/debian/patches/CVE-2024-12084/0001-Some-checksum-buffer-fixes.patch rsync-3.2.7/debian/patches/CVE-2024-12084/0001-Some-checksum-buffer-fixes.patch --- rsync-3.2.7/debian/patches/CVE-2024-12084/0001-Some-checksum-buffer-fixes.patch 2025-11-28 00:49:27.000000000 +0000 +++ rsync-3.2.7/debian/patches/CVE-2024-12084/0001-Some-checksum-buffer-fixes.patch 1970-01-01 00:00:00.000000000 +0000 @@ -1,151 +0,0 @@ -From 0902b52f6687b1f7952422080d50b93108742e53 Mon Sep 17 00:00:00 2001 -From: Wayne Davison -Date: Tue, 29 Oct 2024 22:55:29 -0700 -Subject: [PATCH 1/2] Some checksum buffer fixes. - -- Put sum2_array into sum_struct to hold an array of sum2 checksums - that are each xfer_sum_len bytes. -- Remove sum2 buf from sum_buf. -- Add macro sum2_at() to access each sum2 array element. -- Throw an error if a sums header has an s2length larger than - xfer_sum_len. ---- - io.c | 3 ++- - match.c | 8 ++++---- - rsync.c | 5 ++++- - rsync.h | 4 +++- - sender.c | 4 +++- - 5 files changed, 16 insertions(+), 8 deletions(-) - -diff --git a/io.c b/io.c -index a99ac0ec..bb60eeca 100644 ---- a/io.c -+++ b/io.c -@@ -55,6 +55,7 @@ extern int read_batch; - extern int compat_flags; - extern int protect_args; - extern int checksum_seed; -+extern int xfer_sum_len; - extern int daemon_connection; - extern int protocol_version; - extern int remove_source_files; -@@ -1977,7 +1978,7 @@ void read_sum_head(int f, struct sum_struct *sum) - exit_cleanup(RERR_PROTOCOL); - } - sum->s2length = protocol_version < 27 ? csum_length : (int)read_int(f); -- if (sum->s2length < 0 || sum->s2length > MAX_DIGEST_LEN) { -+ if (sum->s2length < 0 || sum->s2length > xfer_sum_len) { - rprintf(FERROR, "Invalid checksum length %d [%s]\n", - sum->s2length, who_am_i()); - exit_cleanup(RERR_PROTOCOL); -diff --git a/match.c b/match.c -index cdb30a15..36e78ed2 100644 ---- a/match.c -+++ b/match.c -@@ -232,7 +232,7 @@ static void hash_search(int f,struct sum_struct *s, - done_csum2 = 1; - } - -- if (memcmp(sum2,s->sums[i].sum2,s->s2length) != 0) { -+ if (memcmp(sum2, sum2_at(s, i), s->s2length) != 0) { - false_alarms++; - continue; - } -@@ -252,7 +252,7 @@ static void hash_search(int f,struct sum_struct *s, - if (i != aligned_i) { - if (sum != s->sums[aligned_i].sum1 - || l != s->sums[aligned_i].len -- || memcmp(sum2, s->sums[aligned_i].sum2, s->s2length) != 0) -+ || memcmp(sum2, sum2_at(s, aligned_i), s->s2length) != 0) - goto check_want_i; - i = aligned_i; - } -@@ -271,7 +271,7 @@ static void hash_search(int f,struct sum_struct *s, - if (sum != s->sums[i].sum1) - goto check_want_i; - get_checksum2((char *)map, l, sum2); -- if (memcmp(sum2, s->sums[i].sum2, s->s2length) != 0) -+ if (memcmp(sum2, sum2_at(s, i), s->s2length) != 0) - goto check_want_i; - /* OK, we have a re-alignment match. Bump the offset - * forward to the new match point. */ -@@ -290,7 +290,7 @@ static void hash_search(int f,struct sum_struct *s, - && (!updating_basis_file || s->sums[want_i].offset >= offset - || s->sums[want_i].flags & SUMFLG_SAME_OFFSET) - && sum == s->sums[want_i].sum1 -- && memcmp(sum2, s->sums[want_i].sum2, s->s2length) == 0) { -+ && memcmp(sum2, sum2_at(s, want_i), s->s2length) == 0) { - /* we've found an adjacent match - the RLL coder - * will be happy */ - i = want_i; -diff --git a/rsync.c b/rsync.c -index cd288f57..b130aba5 100644 ---- a/rsync.c -+++ b/rsync.c -@@ -437,7 +437,10 @@ int read_ndx_and_attrs(int f_in, int f_out, int *iflag_ptr, uchar *type_ptr, cha - */ - void free_sums(struct sum_struct *s) - { -- if (s->sums) free(s->sums); -+ if (s->sums) { -+ free(s->sums); -+ free(s->sum2_array); -+ } - free(s); - } - -diff --git a/rsync.h b/rsync.h -index d3709fe0..8ddbe702 100644 ---- a/rsync.h -+++ b/rsync.h -@@ -958,12 +958,12 @@ struct sum_buf { - uint32 sum1; /**< simple checksum */ - int32 chain; /**< next hash-table collision */ - short flags; /**< flag bits */ -- char sum2[SUM_LENGTH]; /**< checksum */ - }; - - struct sum_struct { - OFF_T flength; /**< total file length */ - struct sum_buf *sums; /**< points to info for each chunk */ -+ char *sum2_array; /**< checksums of length xfer_sum_len */ - int32 count; /**< how many chunks */ - int32 blength; /**< block_length */ - int32 remainder; /**< flength % block_length */ -@@ -982,6 +982,8 @@ struct map_struct { - int status; /* first errno from read errors */ - }; - -+#define sum2_at(s, i) ((s)->sum2_array + ((OFF_T)(i) * xfer_sum_len)) -+ - #define NAME_IS_FILE (0) /* filter name as a file */ - #define NAME_IS_DIR (1<<0) /* filter name as a dir */ - #define NAME_IS_XATTR (1<<2) /* filter name as an xattr */ -diff --git a/sender.c b/sender.c -index 3d4f052e..ab205341 100644 ---- a/sender.c -+++ b/sender.c -@@ -31,6 +31,7 @@ extern int log_before_transfer; - extern int stdout_format_has_i; - extern int logfile_format_has_i; - extern int want_xattr_optim; -+extern int xfer_sum_len; - extern int csum_length; - extern int append_mode; - extern int copy_links; -@@ -94,10 +95,11 @@ static struct sum_struct *receive_sums(int f) - return(s); - - s->sums = new_array(struct sum_buf, s->count); -+ s->sum2_array = new_array(char, s->count * xfer_sum_len); - - for (i = 0; i < s->count; i++) { - s->sums[i].sum1 = read_int(f); -- read_buf(f, s->sums[i].sum2, s->s2length); -+ read_buf(f, sum2_at(s, i), s->s2length); - - s->sums[i].offset = offset; - s->sums[i].flags = 0; --- -2.34.1 - diff -Nru rsync-3.2.7/debian/patches/CVE-2024-12084/0002-Another-cast-when-multiplying-integers.patch rsync-3.2.7/debian/patches/CVE-2024-12084/0002-Another-cast-when-multiplying-integers.patch --- rsync-3.2.7/debian/patches/CVE-2024-12084/0002-Another-cast-when-multiplying-integers.patch 2025-11-28 00:49:27.000000000 +0000 +++ rsync-3.2.7/debian/patches/CVE-2024-12084/0002-Another-cast-when-multiplying-integers.patch 1970-01-01 00:00:00.000000000 +0000 @@ -1,39 +0,0 @@ -From 42e2b56c4ede3ab164f9a5c6dae02aa84606a6c1 Mon Sep 17 00:00:00 2001 -From: Wayne Davison -Date: Tue, 5 Nov 2024 11:01:03 -0800 -Subject: [PATCH 2/2] Another cast when multiplying integers. - ---- - rsync.h | 2 +- - sender.c | 2 +- - 2 files changed, 2 insertions(+), 2 deletions(-) - -diff --git a/rsync.h b/rsync.h -index 8ddbe702..0f9e277f 100644 ---- a/rsync.h -+++ b/rsync.h -@@ -982,7 +982,7 @@ struct map_struct { - int status; /* first errno from read errors */ - }; - --#define sum2_at(s, i) ((s)->sum2_array + ((OFF_T)(i) * xfer_sum_len)) -+#define sum2_at(s, i) ((s)->sum2_array + ((size_t)(i) * xfer_sum_len)) - - #define NAME_IS_FILE (0) /* filter name as a file */ - #define NAME_IS_DIR (1<<0) /* filter name as a dir */ -diff --git a/sender.c b/sender.c -index ab205341..2bbff2fa 100644 ---- a/sender.c -+++ b/sender.c -@@ -95,7 +95,7 @@ static struct sum_struct *receive_sums(int f) - return(s); - - s->sums = new_array(struct sum_buf, s->count); -- s->sum2_array = new_array(char, s->count * xfer_sum_len); -+ s->sum2_array = new_array(char, (size_t)s->count * xfer_sum_len); - - for (i = 0; i < s->count; i++) { - s->sums[i].sum1 = read_int(f); --- -2.34.1 - diff -Nru rsync-3.2.7/debian/patches/CVE-2024-12085/0001-prevent-information-leak-off-the-stack.patch rsync-3.2.7/debian/patches/CVE-2024-12085/0001-prevent-information-leak-off-the-stack.patch --- rsync-3.2.7/debian/patches/CVE-2024-12085/0001-prevent-information-leak-off-the-stack.patch 2025-11-28 00:49:27.000000000 +0000 +++ rsync-3.2.7/debian/patches/CVE-2024-12085/0001-prevent-information-leak-off-the-stack.patch 1970-01-01 00:00:00.000000000 +0000 @@ -1,27 +0,0 @@ -From cf620065502f065d4ea44f5df4f81295a738aa21 Mon Sep 17 00:00:00 2001 -From: Andrew Tridgell -Date: Thu, 14 Nov 2024 09:57:08 +1100 -Subject: [PATCH] prevent information leak off the stack - -prevent leak of uninitialised stack data in hash_search ---- - match.c | 3 +++ - 1 file changed, 3 insertions(+) - -diff --git a/match.c b/match.c -index 36e78ed2..dfd6af2c 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; --- -2.34.1 - diff -Nru rsync-3.2.7/debian/patches/CVE-2024-12086/0001-refuse-fuzzy-options-when-fuzzy-not-selected.patch rsync-3.2.7/debian/patches/CVE-2024-12086/0001-refuse-fuzzy-options-when-fuzzy-not-selected.patch --- rsync-3.2.7/debian/patches/CVE-2024-12086/0001-refuse-fuzzy-options-when-fuzzy-not-selected.patch 2025-11-28 00:49:27.000000000 +0000 +++ rsync-3.2.7/debian/patches/CVE-2024-12086/0001-refuse-fuzzy-options-when-fuzzy-not-selected.patch 1970-01-01 00:00:00.000000000 +0000 @@ -1,37 +0,0 @@ -From 3feb8669d875d03c9ceb82e208ef40ddda8eb908 Mon Sep 17 00:00:00 2001 -From: Andrew Tridgell -Date: Sat, 23 Nov 2024 11:08:03 +1100 -Subject: [PATCH 1/4] refuse fuzzy options when fuzzy not selected - -this prevents a malicious server providing a file to compare to when -the user has not given the fuzzy option ---- - receiver.c | 5 +++++ - 1 file changed, 5 insertions(+) - -diff --git a/receiver.c b/receiver.c -index 6b4b369e..2d7f6033 100644 ---- a/receiver.c -+++ b/receiver.c -@@ -66,6 +66,7 @@ extern char sender_file_sum[MAX_DIGEST_LEN]; - extern struct file_list *cur_flist, *first_flist, *dir_flist; - extern filter_rule_list daemon_filter_list; - extern OFF_T preallocated_len; -+extern int fuzzy_basis; - - extern struct name_num_item *xfer_sum_nni; - extern int xfer_sum_len; -@@ -716,6 +717,10 @@ int recv_files(int f_in, int f_out, char *local_name) - fnamecmp = get_backup_name(fname); - break; - case FNAMECMP_FUZZY: -+ if (fuzzy_basis == 0) { -+ rprintf(FERROR_XFER, "rsync: refusing malicious fuzzy operation for %s\n", xname); -+ exit_cleanup(RERR_PROTOCOL); -+ } - if (file->dirname) { - pathjoin(fnamecmpbuf, sizeof fnamecmpbuf, file->dirname, xname); - fnamecmp = fnamecmpbuf; --- -2.34.1 - diff -Nru rsync-3.2.7/debian/patches/CVE-2024-12086/0002-added-secure_relative_open.patch rsync-3.2.7/debian/patches/CVE-2024-12086/0002-added-secure_relative_open.patch --- rsync-3.2.7/debian/patches/CVE-2024-12086/0002-added-secure_relative_open.patch 2025-11-28 00:49:27.000000000 +0000 +++ rsync-3.2.7/debian/patches/CVE-2024-12086/0002-added-secure_relative_open.patch 1970-01-01 00:00:00.000000000 +0000 @@ -1,103 +0,0 @@ -From 33385aefe4773e7a3982d41995681eb079c92d12 Mon Sep 17 00:00:00 2001 -From: Andrew Tridgell -Date: Sat, 23 Nov 2024 12:26:10 +1100 -Subject: [PATCH 2/4] added secure_relative_open() - -this is an open that enforces no symlink following for all path -components in a relative path ---- - syscall.c | 74 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ - 1 file changed, 74 insertions(+) - -diff --git a/syscall.c b/syscall.c -index d92074aa..a4b7f542 100644 ---- a/syscall.c -+++ b/syscall.c -@@ -33,6 +33,8 @@ - #include - #endif - -+#include "ifuncs.h" -+ - extern int dry_run; - extern int am_root; - extern int am_sender; -@@ -712,3 +714,75 @@ int do_open_nofollow(const char *pathname, int flags) - - return fd; - } -+ -+/* -+ open a file relative to a base directory. The basedir can be NULL, -+ in which case the current working directory is used. The relpath -+ must be a relative path, and the relpath must not contain any -+ elements in the path which follow symlinks (ie. like O_NOFOLLOW, but -+ applies to all path components, not just the last component) -+*/ -+int secure_relative_open(const char *basedir, const char *relpath, int flags, mode_t mode) -+{ -+ if (!relpath || relpath[0] == '/') { -+ // must be a relative path -+ errno = EINVAL; -+ return -1; -+ } -+ -+#if !defined(O_NOFOLLOW) || !defined(O_DIRECTORY) -+ // really old system, all we can do is live with the risks -+ if (!basedir) { -+ return open(relpath, flags, mode); -+ } -+ char fullpath[MAXPATHLEN]; -+ pathjoin(fullpath, sizeof fullpath, basedir, relpath); -+ return open(fullpath, flags, mode); -+#else -+ int dirfd = AT_FDCWD; -+ if (basedir != NULL) { -+ dirfd = openat(AT_FDCWD, basedir, O_RDONLY | O_DIRECTORY); -+ if (dirfd == -1) { -+ return -1; -+ } -+ } -+ int retfd = -1; -+ -+ char *path_copy = my_strdup(relpath, __FILE__, __LINE__); -+ if (!path_copy) { -+ return -1; -+ } -+ -+ for (const char *part = strtok(path_copy, "/"); -+ part != NULL; -+ part = strtok(NULL, "/")) -+ { -+ int next_fd = openat(dirfd, part, O_RDONLY | O_DIRECTORY | O_NOFOLLOW); -+ if (next_fd == -1 && errno == ENOTDIR) { -+ if (strtok(NULL, "/") != NULL) { -+ // this is not the last component of the path -+ errno = ELOOP; -+ goto cleanup; -+ } -+ // this could be the last component of the path, try as a file -+ retfd = openat(dirfd, part, flags | O_NOFOLLOW, mode); -+ goto cleanup; -+ } -+ if (next_fd == -1) { -+ goto cleanup; -+ } -+ if (dirfd != AT_FDCWD) close(dirfd); -+ dirfd = next_fd; -+ } -+ -+ // the path must be a directory -+ errno = EINVAL; -+ -+cleanup: -+ free(path_copy); -+ if (dirfd != AT_FDCWD) { -+ close(dirfd); -+ } -+ return retfd; -+#endif // O_NOFOLLOW, O_DIRECTORY -+} --- -2.34.1 - diff -Nru rsync-3.2.7/debian/patches/CVE-2024-12086/0003-receiver-use-secure_relative_open-for-basis-file.patch rsync-3.2.7/debian/patches/CVE-2024-12086/0003-receiver-use-secure_relative_open-for-basis-file.patch --- rsync-3.2.7/debian/patches/CVE-2024-12086/0003-receiver-use-secure_relative_open-for-basis-file.patch 2025-11-28 00:49:27.000000000 +0000 +++ rsync-3.2.7/debian/patches/CVE-2024-12086/0003-receiver-use-secure_relative_open-for-basis-file.patch 1970-01-01 00:00:00.000000000 +0000 @@ -1,103 +0,0 @@ -From e59ef9939d3f0ccc8f9bab51442989a81be0c914 Mon Sep 17 00:00:00 2001 -From: Andrew Tridgell -Date: Sat, 23 Nov 2024 12:28:13 +1100 -Subject: [PATCH 3/4] receiver: use secure_relative_open() for basis file - -this prevents attacks where the basis file is manipulated by a -malicious sender to gain information about files outside the -destination tree ---- - receiver.c | 42 ++++++++++++++++++++++++++---------------- - 1 file changed, 26 insertions(+), 16 deletions(-) - -diff --git a/receiver.c b/receiver.c -index 2d7f6033..8031b8f4 100644 ---- a/receiver.c -+++ b/receiver.c -@@ -552,6 +552,8 @@ int recv_files(int f_in, int f_out, char *local_name) - progress_init(); - - while (1) { -+ const char *basedir = NULL; -+ - cleanup_disable(); - - /* This call also sets cur_flist. */ -@@ -722,27 +724,29 @@ int recv_files(int f_in, int f_out, char *local_name) - exit_cleanup(RERR_PROTOCOL); - } - if (file->dirname) { -- pathjoin(fnamecmpbuf, sizeof fnamecmpbuf, file->dirname, xname); -- fnamecmp = fnamecmpbuf; -- } else -- fnamecmp = xname; -+ basedir = file->dirname; -+ } -+ fnamecmp = xname; - break; - default: - if (fnamecmp_type > FNAMECMP_FUZZY && fnamecmp_type-FNAMECMP_FUZZY <= basis_dir_cnt) { - fnamecmp_type -= FNAMECMP_FUZZY + 1; - if (file->dirname) { -- stringjoin(fnamecmpbuf, sizeof fnamecmpbuf, -- basis_dir[fnamecmp_type], "/", file->dirname, "/", xname, NULL); -- } else -- pathjoin(fnamecmpbuf, sizeof fnamecmpbuf, basis_dir[fnamecmp_type], xname); -+ pathjoin(fnamecmpbuf, sizeof fnamecmpbuf, basis_dir[fnamecmp_type], file->dirname); -+ basedir = fnamecmpbuf; -+ } else { -+ basedir = basis_dir[fnamecmp_type]; -+ } -+ fnamecmp = xname; - } else if (fnamecmp_type >= basis_dir_cnt) { - rprintf(FERROR, - "invalid basis_dir index: %d.\n", - fnamecmp_type); - exit_cleanup(RERR_PROTOCOL); -- } else -- pathjoin(fnamecmpbuf, sizeof fnamecmpbuf, basis_dir[fnamecmp_type], fname); -- fnamecmp = fnamecmpbuf; -+ } else { -+ basedir = basis_dir[fnamecmp_type]; -+ fnamecmp = fname; -+ } - break; - } - if (!fnamecmp || (daemon_filter_list.head -@@ -765,7 +769,7 @@ int recv_files(int f_in, int f_out, char *local_name) - } - - /* open the file */ -- fd1 = do_open(fnamecmp, O_RDONLY, 0); -+ fd1 = secure_relative_open(basedir, fnamecmp, O_RDONLY, 0); - - if (fd1 == -1 && protocol_version < 29) { - if (fnamecmp != fname) { -@@ -776,14 +780,20 @@ int recv_files(int f_in, int f_out, char *local_name) - - if (fd1 == -1 && basis_dir[0]) { - /* pre-29 allowed only one alternate basis */ -- pathjoin(fnamecmpbuf, sizeof fnamecmpbuf, -- basis_dir[0], fname); -- fnamecmp = fnamecmpbuf; -+ basedir = basis_dir[0]; -+ fnamecmp = fname; - fnamecmp_type = FNAMECMP_BASIS_DIR_LOW; -- fd1 = do_open(fnamecmp, O_RDONLY, 0); -+ fd1 = secure_relative_open(basedir, fnamecmp, O_RDONLY, 0); - } - } - -+ if (basedir) { -+ // for the following code we need the full -+ // path name as a single string -+ pathjoin(fnamecmpbuf, sizeof fnamecmpbuf, basedir, fnamecmp); -+ fnamecmp = fnamecmpbuf; -+ } -+ - one_inplace = inplace_partial && fnamecmp_type == FNAMECMP_PARTIAL_DIR; - updating_basis_or_equiv = one_inplace - || (inplace && (fnamecmp == fname || fnamecmp_type == FNAMECMP_BACKUP)); --- -2.34.1 - diff -Nru rsync-3.2.7/debian/patches/CVE-2024-12086/0004-disallow-.-elements-in-relpath-for-secure_relative_o.patch rsync-3.2.7/debian/patches/CVE-2024-12086/0004-disallow-.-elements-in-relpath-for-secure_relative_o.patch --- rsync-3.2.7/debian/patches/CVE-2024-12086/0004-disallow-.-elements-in-relpath-for-secure_relative_o.patch 2025-11-28 00:49:27.000000000 +0000 +++ rsync-3.2.7/debian/patches/CVE-2024-12086/0004-disallow-.-elements-in-relpath-for-secure_relative_o.patch 1970-01-01 00:00:00.000000000 +0000 @@ -1,37 +0,0 @@ -From c78e53edb802d04f7e4e070fe8314f2544749e7a Mon Sep 17 00:00:00 2001 -From: Andrew Tridgell -Date: Tue, 26 Nov 2024 09:16:31 +1100 -Subject: [PATCH 4/4] disallow ../ elements in relpath for secure_relative_open - ---- - syscall.c | 7 +++++++ - 1 file changed, 7 insertions(+) - -diff --git a/syscall.c b/syscall.c -index a4b7f542..47c5ea57 100644 ---- a/syscall.c -+++ b/syscall.c -@@ -721,6 +721,8 @@ int do_open_nofollow(const char *pathname, int flags) - must be a relative path, and the relpath must not contain any - elements in the path which follow symlinks (ie. like O_NOFOLLOW, but - applies to all path components, not just the last component) -+ -+ The relpath must also not contain any ../ elements in the path - */ - int secure_relative_open(const char *basedir, const char *relpath, int flags, mode_t mode) - { -@@ -729,6 +731,11 @@ int secure_relative_open(const char *basedir, const char *relpath, int flags, mo - errno = EINVAL; - return -1; - } -+ if (strncmp(relpath, "../", 3) == 0 || strstr(relpath, "/../")) { -+ // no ../ elements allowed in the relpath -+ errno = EINVAL; -+ return -1; -+ } - - #if !defined(O_NOFOLLOW) || !defined(O_DIRECTORY) - // really old system, all we can do is live with the risks --- -2.34.1 - diff -Nru rsync-3.2.7/debian/patches/CVE-2024-12087/0001-Refuse-a-duplicate-dirlist.patch rsync-3.2.7/debian/patches/CVE-2024-12087/0001-Refuse-a-duplicate-dirlist.patch --- rsync-3.2.7/debian/patches/CVE-2024-12087/0001-Refuse-a-duplicate-dirlist.patch 2025-11-28 00:49:27.000000000 +0000 +++ rsync-3.2.7/debian/patches/CVE-2024-12087/0001-Refuse-a-duplicate-dirlist.patch 1970-01-01 00:00:00.000000000 +0000 @@ -1,45 +0,0 @@ -From 0ebc19ee486a8e928a68d8f98d07d40f176770aa Mon Sep 17 00:00:00 2001 -From: Wayne Davison -Date: Thu, 14 Nov 2024 15:46:50 -0800 -Subject: [PATCH 1/2] Refuse a duplicate dirlist. - ---- - flist.c | 9 +++++++++ - rsync.h | 1 + - 2 files changed, 10 insertions(+) - -diff --git a/flist.c b/flist.c -index 464d556e..847b1054 100644 ---- a/flist.c -+++ b/flist.c -@@ -2584,6 +2584,15 @@ struct file_list *recv_file_list(int f, int dir_ndx) - init_hard_links(); - #endif - -+ if (inc_recurse && dir_ndx >= 0) { -+ 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"); - flist_expand(flist, FLIST_START_LARGE); - -diff --git a/rsync.h b/rsync.h -index 0f9e277f..b9a7101a 100644 ---- a/rsync.h -+++ b/rsync.h -@@ -84,6 +84,7 @@ - #define FLAG_DUPLICATE (1<<4) /* sender */ - #define FLAG_MISSING_DIR (1<<4) /* generator */ - #define FLAG_HLINKED (1<<5) /* receiver/generator (checked on all types) */ -+#define FLAG_GOT_DIR_FLIST (1<<5)/* sender/receiver/generator - dir_flist only */ - #define FLAG_HLINK_FIRST (1<<6) /* receiver/generator (w/FLAG_HLINKED) */ - #define FLAG_IMPLIED_DIR (1<<6) /* sender/receiver/generator (dirs only) */ - #define FLAG_HLINK_LAST (1<<7) /* receiver/generator */ --- -2.34.1 - diff -Nru rsync-3.2.7/debian/patches/CVE-2024-12087/0002-range-check-dir_ndx-before-use.patch rsync-3.2.7/debian/patches/CVE-2024-12087/0002-range-check-dir_ndx-before-use.patch --- rsync-3.2.7/debian/patches/CVE-2024-12087/0002-range-check-dir_ndx-before-use.patch 2025-11-28 00:49:27.000000000 +0000 +++ rsync-3.2.7/debian/patches/CVE-2024-12087/0002-range-check-dir_ndx-before-use.patch 1970-01-01 00:00:00.000000000 +0000 @@ -1,27 +0,0 @@ -From b3e16be18d582dac1513c0a932d146b36e867b1b Mon Sep 17 00:00:00 2001 -From: Andrew Tridgell -Date: Tue, 26 Nov 2024 16:12:45 +1100 -Subject: [PATCH 2/2] range check dir_ndx before use - ---- - flist.c | 4 ++++ - 1 file changed, 4 insertions(+) - -diff --git a/flist.c b/flist.c -index 847b1054..087f9da6 100644 ---- a/flist.c -+++ b/flist.c -@@ -2585,6 +2585,10 @@ struct file_list *recv_file_list(int f, int dir_ndx) - #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); --- -2.34.1 - diff -Nru rsync-3.2.7/debian/patches/CVE-2024-12088/0001-make-safe-links-stricter.patch rsync-3.2.7/debian/patches/CVE-2024-12088/0001-make-safe-links-stricter.patch --- rsync-3.2.7/debian/patches/CVE-2024-12088/0001-make-safe-links-stricter.patch 2025-11-28 00:49:27.000000000 +0000 +++ rsync-3.2.7/debian/patches/CVE-2024-12088/0001-make-safe-links-stricter.patch 1970-01-01 00:00:00.000000000 +0000 @@ -1,136 +0,0 @@ -From 535f8f816539ba681ef0f12015d2cb587ae61b6d Mon Sep 17 00:00:00 2001 -From: Andrew Tridgell -Date: Sat, 23 Nov 2024 15:15:53 +1100 -Subject: [PATCH] make --safe-links stricter - -when --safe-links is used also reject links where a '../' component is -included in the destination as other than the leading part of the -filename ---- - testsuite/safe-links.test | 55 ++++++++++++++++++++++++++++++++++++ - testsuite/unsafe-byname.test | 2 +- - util1.c | 26 ++++++++++++++++- - 3 files changed, 81 insertions(+), 2 deletions(-) - create mode 100644 testsuite/safe-links.test - -diff --git a/testsuite/safe-links.test b/testsuite/safe-links.test -new file mode 100644 -index 00000000..6e95a4b9 ---- /dev/null -+++ b/testsuite/safe-links.test -@@ -0,0 +1,55 @@ -+#!/bin/sh -+ -+. "$suitedir/rsync.fns" -+ -+test_symlink() { -+ is_a_link "$1" || test_fail "File $1 is not a symlink" -+} -+ -+test_regular() { -+ if [ ! -f "$1" ]; then -+ test_fail "File $1 is not regular file or not exists" -+ fi -+} -+ -+test_notexist() { -+ if [ -e "$1" ]; then -+ test_fail "File $1 exists" -+ fi -+ if [ -h "$1" ]; then -+ test_fail "File $1 exists as a symlink" -+ fi -+} -+ -+cd "$tmpdir" -+ -+mkdir from -+ -+mkdir "from/safe" -+mkdir "from/unsafe" -+ -+mkdir "from/safe/files" -+mkdir "from/safe/links" -+ -+touch "from/safe/files/file1" -+touch "from/safe/files/file2" -+touch "from/unsafe/unsafefile" -+ -+ln -s ../files/file1 "from/safe/links/" -+ln -s ../files/file2 "from/safe/links/" -+ln -s ../../unsafe/unsafefile "from/safe/links/" -+ln -s a/a/a/../../../unsafe2 "from/safe/links/" -+ -+#echo "LISTING FROM" -+#ls -lR from -+ -+echo "rsync with relative path and just -a" -+$RSYNC -avv --safe-links from/safe/ to -+ -+#echo "LISTING TO" -+#ls -lR to -+ -+test_symlink to/links/file1 -+test_symlink to/links/file2 -+test_notexist to/links/unsafefile -+test_notexist to/links/unsafe2 -diff --git a/testsuite/unsafe-byname.test b/testsuite/unsafe-byname.test -index 75e72014..d2e318ef 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/util1.c b/util1.c -index da50ff1e..f260d398 100644 ---- a/util1.c -+++ b/util1.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. */ --- -2.34.1 - diff -Nru rsync-3.2.7/debian/patches/CVE-2024-12747/0001-fixed-symlink-race-condition-in-sender.patch rsync-3.2.7/debian/patches/CVE-2024-12747/0001-fixed-symlink-race-condition-in-sender.patch --- rsync-3.2.7/debian/patches/CVE-2024-12747/0001-fixed-symlink-race-condition-in-sender.patch 2025-11-28 00:49:27.000000000 +0000 +++ rsync-3.2.7/debian/patches/CVE-2024-12747/0001-fixed-symlink-race-condition-in-sender.patch 1970-01-01 00:00:00.000000000 +0000 @@ -1,187 +0,0 @@ -From f45f48055e548851bc7230f454dfeba139be6c04 Mon Sep 17 00:00:00 2001 -From: Andrew Tridgell -Date: Wed, 18 Dec 2024 08:59:42 +1100 -Subject: [PATCH] fixed symlink race condition in sender - -when we open a file that we don't expect to be a symlink use -O_NOFOLLOW to prevent a race condition where an attacker could change -a file between being a normal file and a symlink ---- - checksum.c | 2 +- - flist.c | 2 +- - generator.c | 4 ++-- - receiver.c | 2 +- - sender.c | 2 +- - syscall.c | 20 ++++++++++++++++++++ - t_unsafe.c | 3 +++ - tls.c | 3 +++ - trimslash.c | 2 ++ - util1.c | 2 +- - 10 files changed, 35 insertions(+), 7 deletions(-) - -diff --git a/checksum.c b/checksum.c -index cb21882c..66e80896 100644 ---- a/checksum.c -+++ b/checksum.c -@@ -406,7 +406,7 @@ void file_checksum(const char *fname, const STRUCT_STAT *st_p, char *sum) - int32 remainder; - int fd; - -- fd = do_open(fname, O_RDONLY, 0); -+ fd = do_open_checklinks(fname); - if (fd == -1) { - memset(sum, 0, file_sum_len); - return; -diff --git a/flist.c b/flist.c -index 087f9da6..17832533 100644 ---- a/flist.c -+++ b/flist.c -@@ -1390,7 +1390,7 @@ struct file_struct *make_file(const char *fname, struct file_list *flist, - - if (copy_devices && am_sender && IS_DEVICE(st.st_mode)) { - if (st.st_size == 0) { -- int fd = do_open(fname, O_RDONLY, 0); -+ int fd = do_open_checklinks(fname); - if (fd >= 0) { - st.st_size = get_device_size(fd, fname); - close(fd); -diff --git a/generator.c b/generator.c -index 110db28f..3f13bb95 100644 ---- a/generator.c -+++ b/generator.c -@@ -1798,7 +1798,7 @@ static void recv_generator(char *fname, struct file_struct *file, int ndx, - - if (write_devices && IS_DEVICE(sx.st.st_mode) && sx.st.st_size == 0) { - /* This early open into fd skips the regular open below. */ -- if ((fd = do_open(fnamecmp, O_RDONLY, 0)) >= 0) -+ if ((fd = do_open_nofollow(fnamecmp, O_RDONLY)) >= 0) - real_sx.st.st_size = sx.st.st_size = get_device_size(fd, fnamecmp); - } - -@@ -1867,7 +1867,7 @@ static void recv_generator(char *fname, struct file_struct *file, int ndx, - } - - /* open the file */ -- if (fd < 0 && (fd = do_open(fnamecmp, O_RDONLY, 0)) < 0) { -+ if (fd < 0 && (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 8031b8f4..edfbb210 100644 ---- a/receiver.c -+++ b/receiver.c -@@ -775,7 +775,7 @@ int recv_files(int f_in, int f_out, char *local_name) - if (fnamecmp != fname) { - fnamecmp = fname; - fnamecmp_type = 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 2bbff2fa..a4d46c39 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 && protocol_version < 28 ? FERROR : FWARNING; -diff --git a/syscall.c b/syscall.c -index 081357bb..8cea2900 100644 ---- a/syscall.c -+++ b/syscall.c -@@ -45,6 +45,8 @@ extern int preallocate_files; - extern int preserve_perms; - extern int preserve_executability; - extern int open_noatime; -+extern int copy_links; -+extern int copy_unsafe_links; - - #ifndef S_BLKSIZE - # if defined hpux || defined __hpux__ || defined __hpux -@@ -788,3 +790,21 @@ cleanup: - return retfd; - #endif // O_NOFOLLOW, O_DIRECTORY - } -+ -+/* -+ 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 010cac50..e10619a2 100644 ---- a/t_unsafe.c -+++ b/t_unsafe.c -@@ -28,6 +28,9 @@ int am_root = 0; - int am_sender = 1; - int read_only = 0; - int list_only = 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 e6b0708a..858f8f10 100644 ---- a/tls.c -+++ b/tls.c -@@ -49,6 +49,9 @@ int list_only = 0; - int link_times = 0; - int link_owner = 0; - int nsec_times = 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 1ec928ca..f2774cd7 100644 ---- a/trimslash.c -+++ b/trimslash.c -@@ -26,6 +26,8 @@ int am_root = 0; - int am_sender = 1; - int read_only = 1; - int list_only = 0; -+int copy_links = 0; -+int copy_unsafe_links = 0; - - int - main(int argc, char **argv) -diff --git a/util1.c b/util1.c -index f260d398..d84bc414 100644 ---- a/util1.c -+++ b/util1.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; --- -2.34.1 - diff -Nru rsync-3.2.7/debian/patches/CVE-2025-10158.patch rsync-3.2.7/debian/patches/CVE-2025-10158.patch --- rsync-3.2.7/debian/patches/CVE-2025-10158.patch 2025-11-28 00:49:27.000000000 +0000 +++ rsync-3.2.7/debian/patches/CVE-2025-10158.patch 1970-01-01 00:00:00.000000000 +0000 @@ -1,26 +0,0 @@ -From: Andrew Tridgell -Date: Sat, 23 Aug 2025 17:26:53 +1000 -Subject: 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 a4d46c3..b1588b7 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)) { diff -Nru rsync-3.2.7/debian/patches/Fix_use-after-free_in_generator.patch rsync-3.2.7/debian/patches/Fix_use-after-free_in_generator.patch --- rsync-3.2.7/debian/patches/Fix_use-after-free_in_generator.patch 2025-11-28 00:49:27.000000000 +0000 +++ rsync-3.2.7/debian/patches/Fix_use-after-free_in_generator.patch 1970-01-01 00:00:00.000000000 +0000 @@ -1,31 +0,0 @@ -From f923b19fd85039a2b0e908391074872334646d51 Mon Sep 17 00:00:00 2001 -From: Natanael Copa -Date: Wed, 15 Jan 2025 15:48:04 +0100 -Subject: [PATCH] Fix use-after-free in generator - -full_fname() will free the return value in the next call so we need to -duplicate it before passing it to rsyserr. - -Fixes: https://github.com/RsyncProject/rsync/issues/704 ---- - generator.c | 6 +++++- - 1 file changed, 5 insertions(+), 1 deletion(-) - -diff --git a/generator.c b/generator.c -index 3f13bb95..b56fa569 100644 ---- a/generator.c -+++ b/generator.c -@@ -2041,8 +2041,12 @@ int atomic_create(struct file_struct *file, char *fname, const char *slnk, const - - if (!skip_atomic) { - if (do_rename(tmpname, fname) < 0) { -+ char *full_tmpname = strdup(full_fname(tmpname)); -+ if (full_tmpname == NULL) -+ out_of_memory("atomic_create"); - rsyserr(FERROR_XFER, errno, "rename %s -> \"%s\" failed", -- full_fname(tmpname), full_fname(fname)); -+ full_tmpname, full_fname(fname)); -+ free(full_tmpname); - do_unlink(tmpname); - return 0; - } diff -Nru rsync-3.2.7/debian/patches/fix-aclocalm4-include-paths.patch rsync-3.2.7/debian/patches/fix-aclocalm4-include-paths.patch --- rsync-3.2.7/debian/patches/fix-aclocalm4-include-paths.patch 1970-01-01 00:00:00.000000000 +0000 +++ rsync-3.2.7/debian/patches/fix-aclocalm4-include-paths.patch 2026-05-20 06:06:57.000000000 +0000 @@ -0,0 +1,16 @@ +Description: aclocal.m4: Fix relative paths for incluion of m4/{have_type,header_major_fixed,socklen_t}.m4 +Author: Salvatore Bonaccorso +Forwarded: not-needed + +--- a/aclocal.m4 ++++ b/aclocal.m4 +@@ -12,6 +12,6 @@ + # PARTICULAR PURPOSE. + + m4_ifndef([AC_CONFIG_MACRO_DIRS], [m4_defun([_AM_CONFIG_MACRO_DIRS], [])m4_defun([AC_CONFIG_MACRO_DIRS], [_AM_CONFIG_MACRO_DIRS($@)])]) +-m4_include([../m4/have_type.m4]) +-m4_include([../m4/header_major_fixed.m4]) +-m4_include([../m4/socklen_t.m4]) ++m4_include([m4/have_type.m4]) ++m4_include([m4/header_major_fixed.m4]) ++m4_include([m4/socklen_t.m4]) diff -Nru rsync-3.2.7/debian/patches/series rsync-3.2.7/debian/patches/series --- rsync-3.2.7/debian/patches/series 2025-11-28 00:49:27.000000000 +0000 +++ rsync-3.2.7/debian/patches/series 2026-05-20 05:48:49.000000000 +0000 @@ -4,19 +4,60 @@ fix_rrsync_man_generation.patch avoid_quoting_of_tilde_when_its_a_destination_arg.patch trust_the_sender_on_a_local_transfer.patch -CVE-2024-12084/0001-Some-checksum-buffer-fixes.patch -CVE-2024-12084/0002-Another-cast-when-multiplying-integers.patch -CVE-2024-12085/0001-prevent-information-leak-off-the-stack.patch -CVE-2024-12086/0001-refuse-fuzzy-options-when-fuzzy-not-selected.patch -CVE-2024-12086/0002-added-secure_relative_open.patch -CVE-2024-12086/0003-receiver-use-secure_relative_open-for-basis-file.patch -CVE-2024-12086/0004-disallow-.-elements-in-relpath-for-secure_relative_o.patch -CVE-2024-12087/0001-Refuse-a-duplicate-dirlist.patch -CVE-2024-12087/0002-range-check-dir_ndx-before-use.patch -CVE-2024-12088/0001-make-safe-links-stricter.patch -CVE-2024-12747/0001-fixed-symlink-race-condition-in-sender.patch raise-protocol-version-to-32.patch +2026-05-20/0001-Fix-warning-about-conflicting-lseek-lseek64-prototyp.patch +2026-05-20/0002-hlink-Fix-function-pointer-cast-in-qsort.patch +2026-05-20/0003-bool-is-a-keyword-in-C23.patch +2026-05-20/0004-Fix-warning-about-missing-bomb-.-prototype.patch +2026-05-20/0005-Some-checksum-buffer-fixes.patch +2026-05-20/0006-Another-cast-when-multiplying-integers.patch +2026-05-20/0007-prevent-information-leak-off-the-stack.patch +2026-05-20/0008-refuse-fuzzy-options-when-fuzzy-not-selected.patch +2026-05-20/0009-added-secure_relative_open.patch +2026-05-20/0010-receiver-use-secure_relative_open-for-basis-file.patch +2026-05-20/0011-disallow-.-elements-in-relpath-for-secure_relative_o.patch +2026-05-20/0012-Refuse-a-duplicate-dirlist.patch +2026-05-20/0013-range-check-dir_ndx-before-use.patch +2026-05-20/0014-make-safe-links-stricter.patch +2026-05-20/0015-fixed-symlink-race-condition-in-sender.patch +2026-05-20/0016-syscall-fix-a-Y2038-bug-by-replacing-Int32x32To64-wi.patch +2026-05-20/0017-options.c-Fix-segv-if-poptGetContext-returns-NULL.patch +2026-05-20/0018-Using-a-correct-time-in-log-file.patch +2026-05-20/0019-configure.ac-check-for-xattr-support-both-in-libc-an.patch +2026-05-20/0020-util-fixed-issue-in-clean_fname.patch +2026-05-20/0021-testsuite-added-clean-fname-underflow-test.patch +2026-05-20/0022-fixed-an-invalid-access-to-files-array.patch +2026-05-20/0023-fix-uninitialized-buf1-in-get_checksum2-MD4-path.patch +2026-05-20/0024-reject-negative-token-values-in-compressed-stream-re.patch +2026-05-20/0025-acl-fixed-ACL-ID-mapping-for-non-root.patch +2026-05-20/0026-fix-uninitialized-mul_one-in-AVX2-checksum-and-add-S.patch +2026-05-20/0027-Fix-glibc-2.43-constness-warnings.patch +2026-05-20/0028-zlib-convert-K-R-function-definitions-to-ANSI-style.patch +2026-05-20/0029-fix-signed-integer-overflow-in-proxy-protocol-v2-hea.patch +2026-05-20/0030-zero-all-new-memory-from-allocations.patch +2026-05-20/0031-xattrs-fixed-count-in-qsort.patch +2026-05-20/0032-call-tzset-before-chroot-to-cache-timezone-data.patch +2026-05-20/0033-testsuite-xattrs-ignore-SUNWattr_-in-the-Solaris-xls.patch +2026-05-20/0034-syscall-use-openat2-RESOLVE_BENEATH-on-Linux-for-sec.patch +2026-05-20/0035-syscall-also-use-O_RESOLVE_BENEATH-on-FreeBSD-and-Ma.patch +2026-05-20/0036-testsuite-skip-symlink-dirlink-basis-on-platforms-wi.patch +2026-05-20/0037-syscall-clientserver-am_chrooted-and-use_secure_syml.patch +2026-05-20/0038-sender-fix-read-path-TOCTOU-by-opening-from-module-r.patch +2026-05-20/0039-syscall-receiver-secure-receiver-side-do_chmod-again.patch +2026-05-20/0040-util1-secure-change_dir-against-symlink-race-chdir-e.patch +2026-05-20/0041-syscall-add-symlink-race-safe-do_-_at-wrappers-and-h.patch +2026-05-20/0042-util1-syscall-secure-copy_file-source-dest-opens-bar.patch +2026-05-20/0043-testsuite-end-to-end-regression-test-for-chdir-symli.patch +2026-05-20/0044-token-harden-compressed-token-decoding-against-integ.patch +2026-05-20/0045-testsuite-cover-refuse-options-compress-for-the-daem.patch +2026-05-20/0046-receiver-add-parent_ndx-0-guard-mirroring-797e17f.patch +2026-05-20/0047-clientserver-fix-hostname-ACL-bypass-when-using-daem.patch +2026-05-20/0048-defence-in-depth-bound-wire-supplied-counts-and-leng.patch +2026-05-20/0049-defence-in-depth-guard-cumulative-snprintf-against-l.patch +2026-05-20/0050-defence-in-depth-receiver-block-index-bounds-read_de.patch +2026-05-20/0052-exclude-fix-crashes-with-fortified-strlcpy.patch +2026-05-20/0053-testsuite-use-integer-sleep-in-clean-fname-underflow.patch +2026-05-20/0055-popt-fix-poptDupArgv-strlcpy-size-argument.patch # Regression from CVE-2024-12087 (#1093052) Fix-FLAG_GOT_DIR_FLIST-collission-with-FLAG_HLINKED.patch -Fix_use-after-free_in_generator.patch -CVE-2025-10158.patch +fix-aclocalm4-include-paths.patch diff -Nru rsync-3.2.7/debian/tests/upstream-tests rsync-3.2.7/debian/tests/upstream-tests --- rsync-3.2.7/debian/tests/upstream-tests 2025-11-28 00:49:27.000000000 +0000 +++ rsync-3.2.7/debian/tests/upstream-tests 2026-05-20 05:47:50.000000000 +0000 @@ -13,7 +13,7 @@ ./configure.sh "$CROSS_COMPILE" 2>/dev/null # Supress gcc warnings (autopkg treats them as failures) -make tls getgroups getfsdev trimslash t_unsafe wildtest testrun 2>/dev/null +make tls getgroups getfsdev t_chmod_secure t_secure_relpath trimslash t_unsafe wildtest testrun 2>/dev/null # Run tests rsync_bin="/usr/bin/rsync" ./runtests.sh diff -Nru rsync-3.2.7/debian/tests/upstream-tests-as-root rsync-3.2.7/debian/tests/upstream-tests-as-root --- rsync-3.2.7/debian/tests/upstream-tests-as-root 2025-11-28 00:49:27.000000000 +0000 +++ rsync-3.2.7/debian/tests/upstream-tests-as-root 2026-05-20 05:47:50.000000000 +0000 @@ -13,7 +13,7 @@ ./configure.sh "$CROSS_COMPILE" 2>/dev/null # Supress gcc warnings (autopkg treats them as failures) -make tls getgroups getfsdev trimslash t_unsafe wildtest testrun 2>/dev/null +make tls getgroups getfsdev t_chmod_secure t_secure_relpath trimslash t_unsafe wildtest testrun 2>/dev/null # Run tests rsync_bin="/usr/bin/rsync" ./runtests.sh