* [OE-core][scarthgap][PATCH 1/6] rsync: Fix CVE-2026-29518
@ 2026-06-12 12:13 Ashishkumar Parmar X (asparmar - E INFOCHIPS PRIVATE LIMITED at Cisco)
2026-06-12 12:13 ` [OE-core][scarthgap][PATCH 2/6] rsync: Fix CVE-2026-43619 Ashishkumar Parmar X (asparmar - E INFOCHIPS PRIVATE LIMITED at Cisco)
` (7 more replies)
0 siblings, 8 replies; 15+ messages in thread
From: Ashishkumar Parmar X (asparmar - E INFOCHIPS PRIVATE LIMITED at Cisco) @ 2026-06-12 12:13 UTC (permalink / raw)
To: openembedded-core; +Cc: xe-linux-external, Ashishkumar Parmar
From: Ashishkumar Parmar <asparmar@cisco.com>
Pick the upstream backport [1] for CVE-2026-29518 as mentioned in [3],
where a non-chrooted rsync daemon could be exposed to a parent path
TOCTOU race that allowed file access outside the module.
Also include the dependent upstream fix that followed the CVE fix:
- CVE-2026-29518_p2.patch [2] secures sender read-path opens by
opening files from the module root, closing the same race on the
read side.
[1] https://github.com/RsyncProject/rsync/commit/1a5ad81add1004354a3d8ba841b94ffe19cd2505
[2] https://github.com/RsyncProject/rsync/commit/99b36291d06ca66229942c7a525a1f5566f10c85
[3] https://www.cve.org/CVERecord?id=CVE-2026-29518
Signed-off-by: Ashishkumar Parmar <asparmar@cisco.com>
---
.../rsync/files/CVE-2026-29518_p1.patch | 330 ++++++++++++++++++
.../rsync/files/CVE-2026-29518_p2.patch | 73 ++++
meta/recipes-devtools/rsync/rsync_3.2.7.bb | 2 +
3 files changed, 405 insertions(+)
create mode 100644 meta/recipes-devtools/rsync/files/CVE-2026-29518_p1.patch
create mode 100644 meta/recipes-devtools/rsync/files/CVE-2026-29518_p2.patch
diff --git a/meta/recipes-devtools/rsync/files/CVE-2026-29518_p1.patch b/meta/recipes-devtools/rsync/files/CVE-2026-29518_p1.patch
new file mode 100644
index 0000000000..227ca56dd3
--- /dev/null
+++ b/meta/recipes-devtools/rsync/files/CVE-2026-29518_p1.patch
@@ -0,0 +1,330 @@
+From c5192e125999130b7e15c621989839da31b15a05 Mon Sep 17 00:00:00 2001
+From: Andrew Tridgell <andrew@tridgell.net>
+Date: Wed, 31 Dec 2025 10:01:23 +1100
+Subject: [PATCH] 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) <noreply@anthropic.com>
+
+CVE: CVE-2026-29518
+Upstream-Status: Backport [https://github.com/RsyncProject/rsync/commit/1a5ad81add1004354a3d8ba841b94ffe19cd2505]
+
+(cherry picked from commit 1a5ad81add1004354a3d8ba841b94ffe19cd2505)
+Signed-off-by: Ashishkumar Parmar <asparmar@cisco.com>
+---
+ 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 7c897abc..b6eba098 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;
+@@ -981,6 +983,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;
+ }
+
+@@ -1003,6 +1006,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])) {
+@@ -1305,6 +1317,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 d38bbe8d..d4ca5396 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 8aab2cc0..8b39a6e2 100644
+--- a/syscall.c
++++ b/syscall.c
+@@ -882,6 +882,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.35.6
diff --git a/meta/recipes-devtools/rsync/files/CVE-2026-29518_p2.patch b/meta/recipes-devtools/rsync/files/CVE-2026-29518_p2.patch
new file mode 100644
index 0000000000..4ce3d03248
--- /dev/null
+++ b/meta/recipes-devtools/rsync/files/CVE-2026-29518_p2.patch
@@ -0,0 +1,73 @@
+From 37d459f837868cf5dc6a3f4962c8c9d9bcd2d4b8 Mon Sep 17 00:00:00 2001
+From: Andrew Tridgell <andrew@tridgell.net>
+Date: Sun, 1 Mar 2026 09:28:40 +1100
+Subject: [PATCH] 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 <noreply@anthropic.com>
+
+CVE: CVE-2026-29518
+Upstream-Status: Backport [https://github.com/RsyncProject/rsync/commit/99b36291d06ca66229942c7a525a1f5566f10c85]
+
+(cherry picked from commit 99b36291d06ca66229942c7a525a1f5566f10c85)
+Signed-off-by: Ashishkumar Parmar <asparmar@cisco.com>
+---
+ 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.35.6
diff --git a/meta/recipes-devtools/rsync/rsync_3.2.7.bb b/meta/recipes-devtools/rsync/rsync_3.2.7.bb
index 2a1c3d9d56..fdbee387e3 100644
--- a/meta/recipes-devtools/rsync/rsync_3.2.7.bb
+++ b/meta/recipes-devtools/rsync/rsync_3.2.7.bb
@@ -29,6 +29,8 @@ SRC_URI = "https://download.samba.org/pub/${BPN}/src/${BP}.tar.gz \
file://CVE-2024-12747.patch \
file://CVE-2025-10158.patch \
file://CVE-2026-41035.patch \
+ file://CVE-2026-29518_p1.patch \
+ file://CVE-2026-29518_p2.patch \
"
SRC_URI[sha256sum] = "4e7d9d3f6ed10878c58c5fb724a67dacf4b6aac7340b13e488fb2dc41346f2bb"
--
2.44.1
^ permalink raw reply related [flat|nested] 15+ messages in thread
* [OE-core][scarthgap][PATCH 2/6] rsync: Fix CVE-2026-43619
2026-06-12 12:13 [OE-core][scarthgap][PATCH 1/6] rsync: Fix CVE-2026-29518 Ashishkumar Parmar X (asparmar - E INFOCHIPS PRIVATE LIMITED at Cisco)
@ 2026-06-12 12:13 ` Ashishkumar Parmar X (asparmar - E INFOCHIPS PRIVATE LIMITED at Cisco)
2026-06-12 12:13 ` [OE-core][scarthgap][PATCH 3/6] rsync: Fix CVE-2026-43618 Ashishkumar Parmar X (asparmar - E INFOCHIPS PRIVATE LIMITED at Cisco)
` (6 subsequent siblings)
7 siblings, 0 replies; 15+ messages in thread
From: Ashishkumar Parmar X (asparmar - E INFOCHIPS PRIVATE LIMITED at Cisco) @ 2026-06-12 12:13 UTC (permalink / raw)
To: openembedded-core; +Cc: xe-linux-external, Ashishkumar Parmar
From: Ashishkumar Parmar <asparmar@cisco.com>
Pick the upstream backport [1] for CVE-2026-43619 as mentioned in [8],
where receiver-side chmod could follow symlink-race path escapes.
Also include the upstream dependency fixes needed by the CVE fix:
- CVE-2026-43619-dependent_p1.patch [2] adds
openat2(RESOLVE_BENEATH) based secure path resolution on Linux.
- CVE-2026-43619-dependent_p2.patch [3] adds O_RESOLVE_BENEATH based
secure path resolution on FreeBSD and macOS.
- CVE-2026-43619-dependent_p3.patch [4] skips the regression test on
platforms without an equivalent beneath resolver.
- CVE-2026-43619_p2.patch [5] secures change_dir() against
symlink-race chdir escapes.
- CVE-2026-43619_p3.patch [6] adds symlink-race-safe do_*_at()
wrappers.
- CVE-2026-43619_p4.patch [7] secures copy_file source and
destination opens.
[1] https://github.com/RsyncProject/rsync/commit/24852cda3db38e2f2cd78a13703373c77f75f4d5
[2] https://github.com/RsyncProject/rsync/commit/72d1cf1c288e5c526e906db2edafbf3d55762668
[3] https://github.com/RsyncProject/rsync/commit/61d987c54a472d88855c5fbef3a4c7b51696f93a
[4] https://github.com/RsyncProject/rsync/commit/2b3f8aacc7eca828574a304a0f2b88fbcdaa04e3
[5] https://github.com/RsyncProject/rsync/commit/d22b6bc7d1b1d7be9df1c0c6db1599cb7d5fd82c
[6] https://github.com/RsyncProject/rsync/commit/39b3074a1ab18705cd685fe0659fc958c8cd3db5
[7] https://github.com/RsyncProject/rsync/commit/a277a06b1017b4cf6bb0fe33d5823869ed02dfd9
[8] https://www.cve.org/CVERecord?id=CVE-2026-43619
Signed-off-by: Ashishkumar Parmar <asparmar@cisco.com>
---
.../files/CVE-2026-43619-dependent_p1.patch | 396 ++++
.../files/CVE-2026-43619-dependent_p2.patch | 100 +
.../files/CVE-2026-43619-dependent_p3.patch | 54 +
.../rsync/files/CVE-2026-43619_p1.patch | 471 +++++
.../rsync/files/CVE-2026-43619_p2.patch | 207 ++
.../rsync/files/CVE-2026-43619_p3.patch | 1784 +++++++++++++++++
.../rsync/files/CVE-2026-43619_p4.patch | 570 ++++++
meta/recipes-devtools/rsync/rsync_3.2.7.bb | 7 +
8 files changed, 3589 insertions(+)
create mode 100644 meta/recipes-devtools/rsync/files/CVE-2026-43619-dependent_p1.patch
create mode 100644 meta/recipes-devtools/rsync/files/CVE-2026-43619-dependent_p2.patch
create mode 100644 meta/recipes-devtools/rsync/files/CVE-2026-43619-dependent_p3.patch
create mode 100644 meta/recipes-devtools/rsync/files/CVE-2026-43619_p1.patch
create mode 100644 meta/recipes-devtools/rsync/files/CVE-2026-43619_p2.patch
create mode 100644 meta/recipes-devtools/rsync/files/CVE-2026-43619_p3.patch
create mode 100644 meta/recipes-devtools/rsync/files/CVE-2026-43619_p4.patch
diff --git a/meta/recipes-devtools/rsync/files/CVE-2026-43619-dependent_p1.patch b/meta/recipes-devtools/rsync/files/CVE-2026-43619-dependent_p1.patch
new file mode 100644
index 0000000000..882a0ebbbe
--- /dev/null
+++ b/meta/recipes-devtools/rsync/files/CVE-2026-43619-dependent_p1.patch
@@ -0,0 +1,396 @@
+From e3ee2484239b004c852f71fdb57ca9f91e254689 Mon Sep 17 00:00:00 2001
+From: Andrew Tridgell <andrew@tridgell.net>
+Date: Thu, 30 Apr 2026 08:39:22 +1000
+Subject: [PATCH] syscall: use openat2(RESOLVE_BENEATH) on Linux for
+ secure_relative_open
+
+The CVE fix in commit c35e283 made secure_relative_open() walk every
+component of relpath with O_NOFOLLOW. That blocks every symlink in the
+path, which is stricter than the threat model required: legitimate
+directory symlinks within the destination tree (e.g. when using -K /
+--copy-dirlinks) are also rejected, breaking delta transfers with
+"failed verification -- update discarded". See issue #715.
+
+On Linux 5.6+, openat2(RESOLVE_BENEATH | RESOLVE_NO_MAGICLINKS) gives
+us exactly what we want: the kernel rejects any resolution that would
+escape the starting directory (via "..", absolute paths, or symlinks
+pointing outside dirfd) while still following symlinks that resolve
+within it. /proc magic-links are blocked too.
+
+Use openat2 first; fall back to the existing per-component O_NOFOLLOW
+walk on ENOSYS (kernel < 5.6). The lexical "../" checks at the head
+of the function are kept as defense in depth. The Linux gate is
+plain #ifdef __linux__: the runtime ENOSYS fallback covers the only
+case that actually matters (header present + old kernel), and any
+Linux build environment without linux/openat2.h will fail with a
+clear "no such file" error rather than silently disabling the
+protection.
+
+Verified manually that openat2(RESOLVE_BENEATH) blocks all four
+escape patterns (absolute symlink, ../ symlink, lexical .., absolute
+path) while allowing direct and within-tree symlinks. The new
+testsuite/symlink-dirlink-basis.test (taken from PR #864 by Samuel
+Henrique) exercises the issue #715 regression and passes; full
+make check passes 47/47.
+
+Test: testsuite/symlink-dirlink-basis.test (8 scenarios)
+Fixes: https://github.com/RsyncProject/rsync/issues/715
+
+Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
+
+CVE: CVE-2026-43619
+Upstream-Status: Backport [https://github.com/RsyncProject/rsync/commit/72d1cf1c288e5c526e906db2edafbf3d55762668]
+
+(cherry picked from commit 72d1cf1c288e5c526e906db2edafbf3d55762668)
+Signed-off-by: Ashishkumar Parmar <asparmar@cisco.com>
+---
+ 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 2a4d1f83..7fbfe334 100644
+--- a/syscall.c
++++ b/syscall.c
+@@ -33,6 +33,11 @@
+ #include <sys/syscall.h>
+ #endif
+
++#ifdef __linux__
++#include <sys/syscall.h>
++#include <linux/openat2.h>
++#endif
++
+ #include "ifuncs.h"
+
+ extern int dry_run;
+@@ -720,12 +725,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] == '/') {
+@@ -739,7 +781,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.35.6
diff --git a/meta/recipes-devtools/rsync/files/CVE-2026-43619-dependent_p2.patch b/meta/recipes-devtools/rsync/files/CVE-2026-43619-dependent_p2.patch
new file mode 100644
index 0000000000..5fdf9ff3ba
--- /dev/null
+++ b/meta/recipes-devtools/rsync/files/CVE-2026-43619-dependent_p2.patch
@@ -0,0 +1,100 @@
+From 6a39840c5b45f6c5d303e7e0ddf4ed1c5bbf30fd Mon Sep 17 00:00:00 2001
+From: Andrew Tridgell <andrew@tridgell.net>
+Date: Thu, 30 Apr 2026 08:44:11 +1000
+Subject: [PATCH] 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) <noreply@anthropic.com>
+
+CVE: CVE-2026-43619
+Upstream-Status: Backport [https://github.com/RsyncProject/rsync/commit/61d987c54a472d88855c5fbef3a4c7b51696f93a]
+
+(cherry picked from commit 61d987c54a472d88855c5fbef3a4c7b51696f93a)
+Signed-off-by: Ashishkumar Parmar <asparmar@cisco.com>
+---
+ syscall.c | 40 +++++++++++++++++++++++++++++++++++++---
+ 1 file changed, 37 insertions(+), 3 deletions(-)
+
+diff --git a/syscall.c b/syscall.c
+index 7fbfe334..8aab2cc0 100644
+--- a/syscall.c
++++ b/syscall.c
+@@ -734,9 +734,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.
+ */
+@@ -768,6 +772,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] == '/') {
+@@ -791,6 +821,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.35.6
diff --git a/meta/recipes-devtools/rsync/files/CVE-2026-43619-dependent_p3.patch b/meta/recipes-devtools/rsync/files/CVE-2026-43619-dependent_p3.patch
new file mode 100644
index 0000000000..c42c80ce3d
--- /dev/null
+++ b/meta/recipes-devtools/rsync/files/CVE-2026-43619-dependent_p3.patch
@@ -0,0 +1,54 @@
+From 8464e4322012b0a9e418d8172a710e2717b8ec6e Mon Sep 17 00:00:00 2001
+From: Andrew Tridgell <andrew@tridgell.net>
+Date: Thu, 30 Apr 2026 09:00:09 +1000
+Subject: [PATCH] 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) <noreply@anthropic.com>
+
+CVE: CVE-2026-43619
+Upstream-Status: Backport [https://github.com/RsyncProject/rsync/commit/2b3f8aacc7eca828574a304a0f2b88fbcdaa04e3]
+
+(cherry picked from commit 2b3f8aacc7eca828574a304a0f2b88fbcdaa04e3)
+Signed-off-by: Ashishkumar Parmar <asparmar@cisco.com>
+---
+ 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.35.6
diff --git a/meta/recipes-devtools/rsync/files/CVE-2026-43619_p1.patch b/meta/recipes-devtools/rsync/files/CVE-2026-43619_p1.patch
new file mode 100644
index 0000000000..c667134918
--- /dev/null
+++ b/meta/recipes-devtools/rsync/files/CVE-2026-43619_p1.patch
@@ -0,0 +1,471 @@
+From 7902b4968bb181d0bfd90d0c2ee5349bcff92286 Mon Sep 17 00:00:00 2001
+From: Andrew Tridgell <andrew@tridgell.net>
+Date: Mon, 4 May 2026 21:53:14 +1000
+Subject: [PATCH] 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) <noreply@anthropic.com>
+
+CVE: CVE-2026-43619
+Upstream-Status: Backport [https://github.com/RsyncProject/rsync/commit/24852cda3db38e2f2cd78a13703373c77f75f4d5]
+
+Backport Changes:
+- Adapted Makefile.in test-helper context for rsync 3.2.7, which does not
+ have the upstream simdtest target. No security source change was omitted.
+
+(cherry picked from commit 24852cda3db38e2f2cd78a13703373c77f75f4d5)
+Signed-off-by: Ashishkumar Parmar <asparmar@cisco.com>
+---
+ 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 7c80ba66..bc020b1b 100644
+--- a/Makefile.in
++++ b/Makefile.in
+@@ -62,12 +62,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)
++ testrun$(EXEEXT) trimslash$(EXEEXT) t_unsafe$(EXEEXT) t_chmod_secure$(EXEEXT) \
++ wildtest$(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:
+@@ -184,6 +184,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 8b39a6e2..bc883b4b 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 <sys/stat.h>
++
++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 <module-dir>\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.35.6
diff --git a/meta/recipes-devtools/rsync/files/CVE-2026-43619_p2.patch b/meta/recipes-devtools/rsync/files/CVE-2026-43619_p2.patch
new file mode 100644
index 0000000000..44f966702e
--- /dev/null
+++ b/meta/recipes-devtools/rsync/files/CVE-2026-43619_p2.patch
@@ -0,0 +1,207 @@
+From 7e54bf145c3aeebaaa8f049ab48fb4f77e000a02 Mon Sep 17 00:00:00 2001
+From: Andrew Tridgell <andrew@tridgell.net>
+Date: Tue, 5 May 2026 14:34:33 +1000
+Subject: [PATCH] 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) <noreply@anthropic.com>
+
+CVE: CVE-2026-43619
+Upstream-Status: Backport [https://github.com/RsyncProject/rsync/commit/d22b6bc7d1b1d7be9df1c0c6db1599cb7d5fd82c]
+
+(cherry picked from commit d22b6bc7d1b1d7be9df1c0c6db1599cb7d5fd82c)
+Signed-off-by: Ashishkumar Parmar <asparmar@cisco.com>
+---
+ 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://<daemon>/<module>/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" <<EOF
++use chroot = no
++$uid_setting
++$gid_setting
++log file = $scratchdir/rsyncd.log
++[upload]
++ path = $mod
++ use chroot = no
++ read only = no
++EOF
++
++# Pull recursively into the symlinked subdir with dry-run + verbose,
++# capturing the daemon's flist (file list) on stdout. If the daemon
++# enumerates /outside, leak_marker.txt will appear in the listing.
++RSYNC_CONNECT_PROG="$RSYNC --config=$conf --daemon" \
++ $RSYNC -nrv rsync://localhost/upload/cd/ "$scratchdir/dst/" \
++ > "$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 d84bc414..6e457d4f 100644
+--- a/util1.c
++++ b/util1.c
+@@ -1112,6 +1112,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;
+
+@@ -1150,10 +1151,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.35.6
diff --git a/meta/recipes-devtools/rsync/files/CVE-2026-43619_p3.patch b/meta/recipes-devtools/rsync/files/CVE-2026-43619_p3.patch
new file mode 100644
index 0000000000..2514bb6186
--- /dev/null
+++ b/meta/recipes-devtools/rsync/files/CVE-2026-43619_p3.patch
@@ -0,0 +1,1784 @@
+From 6071a56075810cef61f3d576ea7fcf055ca4e95c Mon Sep 17 00:00:00 2001
+From: Andrew Tridgell <andrew@tridgell.net>
+Date: Tue, 5 May 2026 15:02:48 +1000
+Subject: [PATCH] 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) <noreply@anthropic.com>
+
+CVE: CVE-2026-43619
+Upstream-Status: Backport [https://github.com/RsyncProject/rsync/commit/39b3074a1ab18705cd685fe0659fc958c8cd3db5]
+
+Backport Changes:
+- Adapted Makefile.in test-helper context for rsync 3.2.7, which does not
+ have the upstream simdtest target. No security source change was omitted.
+
+(cherry picked from commit 39b3074a1ab18705cd685fe0659fc958c8cd3db5)
+Signed-off-by: Ashishkumar Parmar <asparmar@cisco.com>
+---
+ 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 bc020b1b..437aab08 100644
+--- a/Makefile.in
++++ b/Makefile.in
+@@ -63,12 +63,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)
++ t_secure_relpath$(EXEEXT) wildtest$(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:
+@@ -188,6 +188,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 5c26a6b6..f8b9f899 100644
+--- a/hlink.c
++++ b/hlink.c
+@@ -453,7 +453,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 bc883b4b..759e2460 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
+@@ -491,12 +1106,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;
+@@ -513,12 +1142,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;
+
+@@ -534,10 +1176,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;
+@@ -603,6 +1254,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
+@@ -825,6 +1551,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)
+ {
+@@ -838,10 +1588,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);
+@@ -864,10 +1629,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);
+@@ -885,8 +1657,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;
+ }
+@@ -916,15 +1700,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;
+ }
+
+@@ -950,8 +1766,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 <sys/stat.h>
++
++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 <test-dir>\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" <<EOF
++use chroot = no
++log file = $scratchdir/rsyncd.log
++[upload]
++ path = $mod
++ use chroot = no
++ read only = no
++EOF
++
++# Recursive --link-dest push directly into the module root. We
++# avoid pushing into a destination subdir because the receiver
++# would chdir into it before resolving --link-dest, making the
++# relative basedir "cd" resolve in the wrong CWD and masking the
++# bug. The realistic attack pushes into the module root (or the
++# attacker uses a basedir path that resolves correctly from
++# whichever subdir the receiver chdirs into).
++RSYNC_CONNECT_PROG="$RSYNC --config=$conf --daemon" \
++ $RSYNC -rtp --link-dest=cd "$src/" rsync://localhost/upload/ \
++ >/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 6e457d4f..8850019f 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;
+@@ -1329,20 +1329,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.35.6
diff --git a/meta/recipes-devtools/rsync/files/CVE-2026-43619_p4.patch b/meta/recipes-devtools/rsync/files/CVE-2026-43619_p4.patch
new file mode 100644
index 0000000000..204a94bffb
--- /dev/null
+++ b/meta/recipes-devtools/rsync/files/CVE-2026-43619_p4.patch
@@ -0,0 +1,570 @@
+From fd3986353f170f40d611fae9815640d8038cca3c Mon Sep 17 00:00:00 2001
+From: Andrew Tridgell <andrew@tridgell.net>
+Date: Wed, 6 May 2026 09:45:30 +1000
+Subject: [PATCH] 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) <noreply@anthropic.com>
+
+CVE: CVE-2026-43619
+Upstream-Status: Backport [https://github.com/RsyncProject/rsync/commit/a277a06b1017b4cf6bb0fe33d5823869ed02dfd9]
+
+(cherry picked from commit a277a06b1017b4cf6bb0fe33d5823869ed02dfd9)
+Signed-off-by: Ashishkumar Parmar <asparmar@cisco.com>
+---
+ 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 759e2460..98902274 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" <<EOF
++use chroot = no
++log file = $scratchdir/rsyncd.log
++[upload]
++ path = $mod
++ use chroot = no
++ read only = no
++EOF
++
++RSYNC_CONNECT_PROG="$RSYNC --config=$conf --daemon" \
++ $RSYNC --inplace --backup --backup-dir=cd "$src/target.txt" \
++ rsync://localhost/upload/target.txt >/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" <<EOF
++use chroot = no
++log file = $scratchdir/rsyncd.log
++[upload_fake]
++ path = $mod
++ use chroot = no
++ read only = no
++ fake super = yes
++EOF
++
++RSYNC_CONNECT_PROG="$RSYNC --config=$conf --daemon" \
++ $RSYNC -rl "$src/" rsync://localhost/upload_fake/ >/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" <<EOF
++use chroot = no
++log file = $scratchdir/rsyncd.log
++[upload_fake]
++ path = $mod
++ use chroot = no
++ read only = no
++ fake super = yes
++EOF
++
++RSYNC_CONNECT_PROG="$RSYNC --config=$conf --daemon" \
++ $RSYNC -rD "$src/" rsync://localhost/upload_fake/ >/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" <<EOF
++use chroot = no
++log file = $scratchdir/rsyncd.log
++[upload]
++ path = $mod
++ use chroot = no
++ read only = no
++EOF
++
++# --copy-dest push to module root.
++RSYNC_CONNECT_PROG="$RSYNC --config=$conf --daemon" \
++ $RSYNC -rtp --copy-dest=cd "$src/" rsync://localhost/upload/ \
++ >/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 8850019f..d28a8e02 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.35.6
diff --git a/meta/recipes-devtools/rsync/rsync_3.2.7.bb b/meta/recipes-devtools/rsync/rsync_3.2.7.bb
index fdbee387e3..7dd4f7c471 100644
--- a/meta/recipes-devtools/rsync/rsync_3.2.7.bb
+++ b/meta/recipes-devtools/rsync/rsync_3.2.7.bb
@@ -31,6 +31,13 @@ SRC_URI = "https://download.samba.org/pub/${BPN}/src/${BP}.tar.gz \
file://CVE-2026-41035.patch \
file://CVE-2026-29518_p1.patch \
file://CVE-2026-29518_p2.patch \
+ file://CVE-2026-43619-dependent_p1.patch \
+ file://CVE-2026-43619-dependent_p2.patch \
+ file://CVE-2026-43619-dependent_p3.patch \
+ file://CVE-2026-43619_p1.patch \
+ file://CVE-2026-43619_p2.patch \
+ file://CVE-2026-43619_p3.patch \
+ file://CVE-2026-43619_p4.patch \
"
SRC_URI[sha256sum] = "4e7d9d3f6ed10878c58c5fb724a67dacf4b6aac7340b13e488fb2dc41346f2bb"
--
2.44.1
^ permalink raw reply related [flat|nested] 15+ messages in thread
* [OE-core][scarthgap][PATCH 3/6] rsync: Fix CVE-2026-43618
2026-06-12 12:13 [OE-core][scarthgap][PATCH 1/6] rsync: Fix CVE-2026-29518 Ashishkumar Parmar X (asparmar - E INFOCHIPS PRIVATE LIMITED at Cisco)
2026-06-12 12:13 ` [OE-core][scarthgap][PATCH 2/6] rsync: Fix CVE-2026-43619 Ashishkumar Parmar X (asparmar - E INFOCHIPS PRIVATE LIMITED at Cisco)
@ 2026-06-12 12:13 ` Ashishkumar Parmar X (asparmar - E INFOCHIPS PRIVATE LIMITED at Cisco)
2026-06-12 12:13 ` [OE-core][scarthgap][PATCH 4/6] rsync: Fix CVE-2026-43620 Ashishkumar Parmar X (asparmar - E INFOCHIPS PRIVATE LIMITED at Cisco)
` (5 subsequent siblings)
7 siblings, 0 replies; 15+ messages in thread
From: Ashishkumar Parmar X (asparmar - E INFOCHIPS PRIVATE LIMITED at Cisco) @ 2026-06-12 12:13 UTC (permalink / raw)
To: openembedded-core; +Cc: xe-linux-external, Ashishkumar Parmar
From: Ashishkumar Parmar <asparmar@cisco.com>
Pick the upstream backport [1] for CVE-2026-43618 as mentioned in [2],
where compressed-token decoding could overflow the token index.
[1] https://github.com/RsyncProject/rsync/commit/901041dddc9a343ed51f8e2cd3992aed3ae0180c
[2] https://www.cve.org/CVERecord?id=CVE-2026-43618
Signed-off-by: Ashishkumar Parmar <asparmar@cisco.com>
---
.../rsync/files/CVE-2026-43618.patch | 252 ++++++++++++++++++
meta/recipes-devtools/rsync/rsync_3.2.7.bb | 1 +
2 files changed, 253 insertions(+)
create mode 100644 meta/recipes-devtools/rsync/files/CVE-2026-43618.patch
diff --git a/meta/recipes-devtools/rsync/files/CVE-2026-43618.patch b/meta/recipes-devtools/rsync/files/CVE-2026-43618.patch
new file mode 100644
index 0000000000..ed07491b50
--- /dev/null
+++ b/meta/recipes-devtools/rsync/files/CVE-2026-43618.patch
@@ -0,0 +1,252 @@
+From b45912207aed17451adcda954b8bd6689714d2ed Mon Sep 17 00:00:00 2001
+From: Andrew Tridgell <andrew@tridgell.net>
+Date: Wed, 29 Apr 2026 11:10:59 +1000
+Subject: [PATCH] token: harden compressed-token decoding against integer
+ overflow
+
+The receiver's three compressed-token decoders --
+recv_deflated_token (zlib), recv_zstd_token, and
+recv_compressed_token (lz4) -- accumulated rx_token (a 32-bit
+signed counter) without overflow checking. A malicious sender
+could craft a compressed-token stream that walked rx_token past
+INT32_MAX, with careful manipulation leaking process memory
+contents to the wire (environment variables, passwords, heap
+pointers, library pointers -- significantly weakening ASLR
+and facilitating further exploitation).
+
+Cap rx_token at MAX_TOKEN_INDEX = 0x7ffffffe. Fold the
+bookkeeping into recv_compressed_token_num() and
+recv_compressed_token_run() shared by all three decoders. Reject
+negative or out-of-range token values explicitly. Also cap the
+simple_recv_token literal-block length at the source: any
+wire-supplied length > CHUNK_SIZE is ill-formed (the matching
+simple_send_token never writes a chunk larger than CHUNK_SIZE),
+so reject before looping on attacker-controlled bytes.
+
+Reach: an authenticated daemon connection with compression
+enabled (the default for protocols >= 30 when both peers
+advertise it). Disabling compression on the daemon
+("refuse options = compress" in rsyncd.conf) is the available
+workaround.
+
+Reporter: Omar Elsayed (seks99x).
+
+Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
+
+CVE: CVE-2026-43618
+Upstream-Status: Backport [https://github.com/RsyncProject/rsync/commit/901041dddc9a343ed51f8e2cd3992aed3ae0180c]
+
+Backport Changes:
+- Resolved context conflict against existing Scarthgap token
+ hardening by keeping the upstream
+ recv_compressed_token_num() helper call.
+- The upstream deletion hunks for the old inline
+ rx_token < 0 checks were absent or already different
+ in Scarthgap token.c. The backport keeps the
+ helper-based validation and omits those no-op
+ context deletions.
+
+(cherry picked from commit 901041dddc9a343ed51f8e2cd3992aed3ae0180c)
+Signed-off-by: Ashishkumar Parmar <asparmar@cisco.com>
+---
+ receiver.c | 11 +++++-
+ token.c | 102 ++++++++++++++++++++++++++++++-----------------------
+ 2 files changed, 67 insertions(+), 46 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 c108b3af..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,17 +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 (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;
+@@ -616,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();
+ }
+ }
+ }
+@@ -828,17 +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 (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;
+@@ -869,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();
+ }
+ }
+ }
+@@ -992,17 +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 (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);
+@@ -1018,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.35.6
+
diff --git a/meta/recipes-devtools/rsync/rsync_3.2.7.bb b/meta/recipes-devtools/rsync/rsync_3.2.7.bb
index 7dd4f7c471..e232abafc3 100644
--- a/meta/recipes-devtools/rsync/rsync_3.2.7.bb
+++ b/meta/recipes-devtools/rsync/rsync_3.2.7.bb
@@ -38,6 +38,7 @@ SRC_URI = "https://download.samba.org/pub/${BPN}/src/${BP}.tar.gz \
file://CVE-2026-43619_p2.patch \
file://CVE-2026-43619_p3.patch \
file://CVE-2026-43619_p4.patch \
+ file://CVE-2026-43618.patch \
"
SRC_URI[sha256sum] = "4e7d9d3f6ed10878c58c5fb724a67dacf4b6aac7340b13e488fb2dc41346f2bb"
--
2.44.1
^ permalink raw reply related [flat|nested] 15+ messages in thread
* [OE-core][scarthgap][PATCH 4/6] rsync: Fix CVE-2026-43620
2026-06-12 12:13 [OE-core][scarthgap][PATCH 1/6] rsync: Fix CVE-2026-29518 Ashishkumar Parmar X (asparmar - E INFOCHIPS PRIVATE LIMITED at Cisco)
2026-06-12 12:13 ` [OE-core][scarthgap][PATCH 2/6] rsync: Fix CVE-2026-43619 Ashishkumar Parmar X (asparmar - E INFOCHIPS PRIVATE LIMITED at Cisco)
2026-06-12 12:13 ` [OE-core][scarthgap][PATCH 3/6] rsync: Fix CVE-2026-43618 Ashishkumar Parmar X (asparmar - E INFOCHIPS PRIVATE LIMITED at Cisco)
@ 2026-06-12 12:13 ` Ashishkumar Parmar X (asparmar - E INFOCHIPS PRIVATE LIMITED at Cisco)
2026-06-12 12:13 ` [OE-core][scarthgap][PATCH 5/6] rsync: Fix CVE-2026-43617 Ashishkumar Parmar X (asparmar - E INFOCHIPS PRIVATE LIMITED at Cisco)
` (4 subsequent siblings)
7 siblings, 0 replies; 15+ messages in thread
From: Ashishkumar Parmar X (asparmar - E INFOCHIPS PRIVATE LIMITED at Cisco) @ 2026-06-12 12:13 UTC (permalink / raw)
To: openembedded-core; +Cc: xe-linux-external, Ashishkumar Parmar
From: Ashishkumar Parmar <asparmar@cisco.com>
Pick the upstream backport [1] for CVE-2026-43620 as mentioned in [2],
where a malicious rsync server could trigger an out-of-bounds file-list
access in the receiver.
[1] https://github.com/RsyncProject/rsync/commit/0a5fa00fdcbacbebb89daca0ae68ae320f22dc74
[2] https://www.cve.org/CVERecord?id=CVE-2026-43620
Signed-off-by: Ashishkumar Parmar <asparmar@cisco.com>
---
.../rsync/files/CVE-2026-43620.patch | 124 ++++++++++++++++++
meta/recipes-devtools/rsync/rsync_3.2.7.bb | 1 +
2 files changed, 125 insertions(+)
create mode 100644 meta/recipes-devtools/rsync/files/CVE-2026-43620.patch
diff --git a/meta/recipes-devtools/rsync/files/CVE-2026-43620.patch b/meta/recipes-devtools/rsync/files/CVE-2026-43620.patch
new file mode 100644
index 0000000000..dd50f1ebd6
--- /dev/null
+++ b/meta/recipes-devtools/rsync/files/CVE-2026-43620.patch
@@ -0,0 +1,124 @@
+From 6495fc027ad48678c98eab3de4224e29473ccb08 Mon Sep 17 00:00:00 2001
+From: Andrew Tridgell <andrew@tridgell.net>
+Date: Tue, 5 May 2026 16:48:16 +1000
+Subject: [PATCH] receiver: add parent_ndx<0 guard, mirroring 797e17f
+
+Commit 797e17f ("fixed an invalid access to files array") added a
+parent_ndx < 0 guard to send_files() in sender.c, but the visually-
+identical block in recv_files() in receiver.c was not updated. A
+malicious rsync:// server can therefore drive any connecting client
+into the same out-of-bounds dir_flist->files[-1] read followed by a
+file_struct dereference in f_name() one line later.
+
+Reach: protocol-30+ default (inc_recurse) makes flist.c:2745 set
+parent_ndx = -1 on the first received flist when the sender omits a
+leading "." entry; rsync.c flist_for_ndx() does not reject ndx == 0
+in that state because the range check evaluates 0 < 0 = false; and
+read_ndx_and_attrs() only validates ndx with the ITEM_TRANSFER bit
+set, so iflags=ITEM_IS_NEW (or any other non-transfer iflag word)
+bypasses the check.
+
+Apply the same guard receiver-side. Confirmed: the same PoC (a
+minimal Python rsyncd that handshakes with CF_INC_RECURSE, sends a
+no-leading-"." flist, and emits ndx=0 with ITEM_IS_NEW) crashes
+unpatched 3.4.2 with SEGV_MAPERR si_addr=0x4101a-class in the
+receiver child; with this guard it exits cleanly with code 2
+(RERR_PROTOCOL).
+
+The attack surface delta over the sender variant is large:
+the original was malicious-client -> daemon, this is
+malicious-server -> any rsync client doing a normal rsync://
+or remote-shell pull.
+
+Reported by Pratham Gupta (alchemy1729).
+
+Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
+
+CVE: CVE-2026-43620
+Upstream-Status: Backport [https://github.com/RsyncProject/rsync/commit/0a5fa00fdcbacbebb89daca0ae68ae320f22dc74]
+
+(cherry picked from commit 0a5fa00fdcbacbebb89daca0ae68ae320f22dc74)
+Signed-off-by: Ashishkumar Parmar <asparmar@cisco.com>
+---
+ generator.c | 4 ++++
+ io.c | 3 +++
+ receiver.c | 7 ++++++-
+ sender.c | 2 ++
+ 4 files changed, 15 insertions(+), 1 deletion(-)
+
+diff --git a/generator.c b/generator.c
+index b80eb2e3..38f5ad33 100644
+--- a/generator.c
++++ b/generator.c
+@@ -2146,6 +2146,8 @@ void check_for_finished_files(int itemizing, enum logcode code, int check_redo)
+ if (send_failed)
+ ndx = get_hlink_num();
+ flist = flist_for_ndx(ndx, "check_for_finished_files.1");
++ if (ndx < flist->ndx_start)
++ exit_cleanup(RERR_PROTOCOL);
+ file = flist->files[ndx - flist->ndx_start];
+ assert(file->flags & FLAG_HLINKED);
+ if (send_failed)
+@@ -2174,6 +2176,8 @@ void check_for_finished_files(int itemizing, enum logcode code, int check_redo)
+
+ flist = cur_flist;
+ cur_flist = flist_for_ndx(ndx, "check_for_finished_files.2");
++ if (ndx < cur_flist->ndx_start)
++ exit_cleanup(RERR_PROTOCOL);
+
+ file = cur_flist->files[ndx - cur_flist->ndx_start];
+ if (solo_file)
+diff --git a/io.c b/io.c
+index bb60eeca..c654a7ba 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.35.6
diff --git a/meta/recipes-devtools/rsync/rsync_3.2.7.bb b/meta/recipes-devtools/rsync/rsync_3.2.7.bb
index e232abafc3..b1483fc6a6 100644
--- a/meta/recipes-devtools/rsync/rsync_3.2.7.bb
+++ b/meta/recipes-devtools/rsync/rsync_3.2.7.bb
@@ -39,6 +39,7 @@ SRC_URI = "https://download.samba.org/pub/${BPN}/src/${BP}.tar.gz \
file://CVE-2026-43619_p3.patch \
file://CVE-2026-43619_p4.patch \
file://CVE-2026-43618.patch \
+ file://CVE-2026-43620.patch \
"
SRC_URI[sha256sum] = "4e7d9d3f6ed10878c58c5fb724a67dacf4b6aac7340b13e488fb2dc41346f2bb"
--
2.44.1
^ permalink raw reply related [flat|nested] 15+ messages in thread
* [OE-core][scarthgap][PATCH 5/6] rsync: Fix CVE-2026-43617
2026-06-12 12:13 [OE-core][scarthgap][PATCH 1/6] rsync: Fix CVE-2026-29518 Ashishkumar Parmar X (asparmar - E INFOCHIPS PRIVATE LIMITED at Cisco)
` (2 preceding siblings ...)
2026-06-12 12:13 ` [OE-core][scarthgap][PATCH 4/6] rsync: Fix CVE-2026-43620 Ashishkumar Parmar X (asparmar - E INFOCHIPS PRIVATE LIMITED at Cisco)
@ 2026-06-12 12:13 ` Ashishkumar Parmar X (asparmar - E INFOCHIPS PRIVATE LIMITED at Cisco)
2026-06-12 12:13 ` [OE-core][scarthgap][PATCH 6/6] rsync: Fix CVE-2026-45232 Ashishkumar Parmar X (asparmar - E INFOCHIPS PRIVATE LIMITED at Cisco)
` (3 subsequent siblings)
7 siblings, 0 replies; 15+ messages in thread
From: Ashishkumar Parmar X (asparmar - E INFOCHIPS PRIVATE LIMITED at Cisco) @ 2026-06-12 12:13 UTC (permalink / raw)
To: openembedded-core; +Cc: xe-linux-external, Ashishkumar Parmar
From: Ashishkumar Parmar <asparmar@cisco.com>
Pick the upstream backport [1] for CVE-2026-43617 as mentioned in [2],
where hostname-based daemon ACLs could be bypassed when reverse DNS was
performed after daemon chroot.
[1] https://github.com/RsyncProject/rsync/commit/74ea276900779b95ddd1769d1d6ae78b2fd1a790
[2] https://www.cve.org/CVERecord?id=CVE-2026-43617
Signed-off-by: Ashishkumar Parmar <asparmar@cisco.com>
---
.../rsync/files/CVE-2026-43617.patch | 197 ++++++++++++++++++
meta/recipes-devtools/rsync/rsync_3.2.7.bb | 1 +
2 files changed, 198 insertions(+)
create mode 100644 meta/recipes-devtools/rsync/files/CVE-2026-43617.patch
diff --git a/meta/recipes-devtools/rsync/files/CVE-2026-43617.patch b/meta/recipes-devtools/rsync/files/CVE-2026-43617.patch
new file mode 100644
index 0000000000..409e524ea8
--- /dev/null
+++ b/meta/recipes-devtools/rsync/files/CVE-2026-43617.patch
@@ -0,0 +1,197 @@
+From d1b1f430b3c0ee8f7fd8ffb10ac864689a3ed024 Mon Sep 17 00:00:00 2001
+From: Andrew Tridgell <andrew@tridgell.net>
+Date: Wed, 31 Dec 2025 13:50:35 +1100
+Subject: [PATCH] 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 = <localhost-resolved-name>" 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) <noreply@anthropic.com>
+
+CVE: CVE-2026-43617
+Upstream-Status: Backport [https://github.com/RsyncProject/rsync/commit/74ea276900779b95ddd1769d1d6ae78b2fd1a790]
+
+(cherry picked from commit 74ea276900779b95ddd1769d1d6ae78b2fd1a790)
+Signed-off-by: Ashishkumar Parmar <asparmar@cisco.com>
+---
+ 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 b6eba098..3333aa96 100644
+--- a/clientserver.c
++++ b/clientserver.c
+@@ -1310,6 +1310,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" <<EOF
++use chroot = no
++log file = $logfile
++daemon chroot = $chrootdir
++reverse lookup = $1
++hosts deny = $client_hostname
++max verbosity = 4
++
++[chrootmod]
++ path = /modroot
++ read only = yes
++ reverse lookup = $2
++EOF
++}
++
++# Run a transfer and return 0 if the daemon refused with @ERROR access
++# denied (the expected outcome when the deny rule matches).
++run_check() {
++ label="$1"
++
++ rm -f "$logfile"
++ rm -rf "$todir"
++ mkdir -p "$todir"
++
++ out="$scratchdir/run.out"
++
++ RSYNC_CONNECT_PROG="$RSYNC --config=$conf --daemon" \
++ $RSYNC -av localhost::chrootmod/ "$todir/" >"$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.35.6
diff --git a/meta/recipes-devtools/rsync/rsync_3.2.7.bb b/meta/recipes-devtools/rsync/rsync_3.2.7.bb
index b1483fc6a6..a27fb0f291 100644
--- a/meta/recipes-devtools/rsync/rsync_3.2.7.bb
+++ b/meta/recipes-devtools/rsync/rsync_3.2.7.bb
@@ -40,6 +40,7 @@ SRC_URI = "https://download.samba.org/pub/${BPN}/src/${BP}.tar.gz \
file://CVE-2026-43619_p4.patch \
file://CVE-2026-43618.patch \
file://CVE-2026-43620.patch \
+ file://CVE-2026-43617.patch \
"
SRC_URI[sha256sum] = "4e7d9d3f6ed10878c58c5fb724a67dacf4b6aac7340b13e488fb2dc41346f2bb"
--
2.44.1
^ permalink raw reply related [flat|nested] 15+ messages in thread
* [OE-core][scarthgap][PATCH 6/6] rsync: Fix CVE-2026-45232
2026-06-12 12:13 [OE-core][scarthgap][PATCH 1/6] rsync: Fix CVE-2026-29518 Ashishkumar Parmar X (asparmar - E INFOCHIPS PRIVATE LIMITED at Cisco)
` (3 preceding siblings ...)
2026-06-12 12:13 ` [OE-core][scarthgap][PATCH 5/6] rsync: Fix CVE-2026-43617 Ashishkumar Parmar X (asparmar - E INFOCHIPS PRIVATE LIMITED at Cisco)
@ 2026-06-12 12:13 ` Ashishkumar Parmar X (asparmar - E INFOCHIPS PRIVATE LIMITED at Cisco)
2026-06-22 11:31 ` [OE-core][scarthgap][PATCH 1/6] rsync: Fix CVE-2026-29518 Yoann Congal
` (2 subsequent siblings)
7 siblings, 0 replies; 15+ messages in thread
From: Ashishkumar Parmar X (asparmar - E INFOCHIPS PRIVATE LIMITED at Cisco) @ 2026-06-12 12:13 UTC (permalink / raw)
To: openembedded-core; +Cc: xe-linux-external, Ashishkumar Parmar
From: Ashishkumar Parmar <asparmar@cisco.com>
Pick the upstream backport [1] for CVE-2026-45232 as mentioned in [2],
where an over-long proxy response line could write one byte past the
stack buffer.
[1] https://github.com/RsyncProject/rsync/commit/36860669cceaa2ca7cc84367d1fb8a3655560300
[2] https://www.cve.org/CVERecord?id=CVE-2026-45232
Signed-off-by: Ashishkumar Parmar <asparmar@cisco.com>
---
.../rsync/files/CVE-2026-45232.patch | 243 ++++++++++++++++++
meta/recipes-devtools/rsync/rsync_3.2.7.bb | 1 +
2 files changed, 244 insertions(+)
create mode 100644 meta/recipes-devtools/rsync/files/CVE-2026-45232.patch
diff --git a/meta/recipes-devtools/rsync/files/CVE-2026-45232.patch b/meta/recipes-devtools/rsync/files/CVE-2026-45232.patch
new file mode 100644
index 0000000000..d8e6c94b84
--- /dev/null
+++ b/meta/recipes-devtools/rsync/files/CVE-2026-45232.patch
@@ -0,0 +1,243 @@
+From cd379e2b19a9f2fa10cadbe179641acb33edb158 Mon Sep 17 00:00:00 2001
+From: Andrew Tridgell <andrew@tridgell.net>
+Date: Wed, 13 May 2026 20:35:35 +1000
+Subject: [PATCH] socket: reject over-long proxy response line
+
+fixes a one byte stack overflow when using RSYNC_PROXY with a
+malicious proxy.
+
+Reach: only when RSYNC_PROXY is set and a malicious or MITM'd
+proxy returns the pathological response. The byte written is
+always '\0' and the attacker doesn't choose the offset, so impact
+is corruption of one adjacent stack byte and possible later
+misbehaviour or crash -- no information disclosure beyond the
+existing rprintf of buffer contents.
+
+Reported by Aisle Research via Michal Ruprich
+
+CVE: CVE-2026-45232
+Upstream-Status: Backport [https://github.com/RsyncProject/rsync/commit/36860669cceaa2ca7cc84367d1fb8a3655560300]
+
+(cherry picked from commit 36860669cceaa2ca7cc84367d1fb8a3655560300)
+Signed-off-by: Ashishkumar Parmar <asparmar@cisco.com>
+---
+ socket.c | 30 +++--
+ testsuite/proxy-response-line-too-long.test | 128 ++++++++++++++++++++
+ 2 files changed, 145 insertions(+), 13 deletions(-)
+ create mode 100755 testsuite/proxy-response-line-too-long.test
+
+diff --git a/socket.c b/socket.c
+index c2075adf..6a8f6f4a 100644
+--- a/socket.c
++++ b/socket.c
+@@ -47,21 +47,23 @@ static struct sigaction sigact;
+
+ static int sock_exec(const char *prog);
+
++#define PROXY_BUF_SIZE 1024
++
+ /* Establish a proxy connection on an open socket to a web proxy by using the
+ * CONNECT method. If proxy_user and proxy_pass are not NULL, they are used to
+ * authenticate to the proxy using the "Basic" proxy-authorization protocol. */
+ static int establish_proxy_connection(int fd, char *host, int port, char *proxy_user, char *proxy_pass)
+ {
+- char *cp, buffer[1024];
+- char *authhdr, authbuf[1024];
++ char *cp, buffer[PROXY_BUF_SIZE + 1];
++ char *authhdr, authbuf[PROXY_BUF_SIZE + 1];
+ int len;
+
+ if (proxy_user && proxy_pass) {
+- stringjoin(buffer, sizeof buffer,
++ stringjoin(buffer, PROXY_BUF_SIZE,
+ proxy_user, ":", proxy_pass, NULL);
+ len = strlen(buffer);
+
+- if ((len*8 + 5) / 6 >= (int)sizeof authbuf - 3) {
++ if ((len*8 + 5) / 6 >= PROXY_BUF_SIZE - 3) {
+ rprintf(FERROR,
+ "authentication information is too long\n");
+ return -1;
+@@ -74,14 +76,14 @@ static int establish_proxy_connection(int fd, char *host, int port, char *proxy_
+ authhdr = "";
+ }
+
+- len = snprintf(buffer, sizeof buffer, "CONNECT %s:%d HTTP/1.0%s%s\r\n\r\n", host, port, authhdr, authbuf);
+- assert(len > 0 && len < (int)sizeof buffer);
++ len = snprintf(buffer, PROXY_BUF_SIZE, "CONNECT %s:%d HTTP/1.0%s%s\r\n\r\n", host, port, authhdr, authbuf);
++ assert(len > 0 && len < PROXY_BUF_SIZE);
+ if (write(fd, buffer, len) != len) {
+ rsyserr(FERROR, errno, "failed to write to proxy");
+ return -1;
+ }
+
+- for (cp = buffer; cp < &buffer[sizeof buffer - 1]; cp++) {
++ for (cp = buffer; cp < &buffer[PROXY_BUF_SIZE - 1]; cp++) {
+ if (read(fd, cp, 1) != 1) {
+ rsyserr(FERROR, errno, "failed to read from proxy");
+ return -1;
+@@ -90,11 +92,13 @@ static int establish_proxy_connection(int fd, char *host, int port, char *proxy_
+ break;
+ }
+
+- if (*cp != '\n')
+- cp++;
+- *cp-- = '\0';
+- if (*cp == '\r')
+- *cp = '\0';
++ if (cp == &buffer[PROXY_BUF_SIZE - 1]) {
++ rprintf(FERROR, "proxy response line too long\n");
++ return -1;
++ }
++ *cp = '\0';
++ if (cp > buffer && cp[-1] == '\r')
++ cp[-1] = '\0';
+ if (strncmp(buffer, "HTTP/", 5) != 0) {
+ rprintf(FERROR, "bad response from proxy -- %s\n",
+ buffer);
+@@ -110,7 +114,7 @@ static int establish_proxy_connection(int fd, char *host, int port, char *proxy_
+ }
+ /* throw away the rest of the HTTP header */
+ while (1) {
+- for (cp = buffer; cp < &buffer[sizeof buffer - 1]; cp++) {
++ for (cp = buffer; cp < &buffer[PROXY_BUF_SIZE]; cp++) {
+ if (read(fd, cp, 1) != 1) {
+ rsyserr(FERROR, errno,
+ "failed to read from proxy");
+diff --git a/testsuite/proxy-response-line-too-long.test b/testsuite/proxy-response-line-too-long.test
+new file mode 100755
+index 00000000..7f55c43b
+--- /dev/null
++++ b/testsuite/proxy-response-line-too-long.test
+@@ -0,0 +1,128 @@
++#!/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 off-by-one stack OOB write in
++# establish_proxy_connection() in socket.c when a malicious or
++# man-in-the-middle HTTP proxy returns a first response line of
++# 1023+ bytes without a '\n' terminator.
++#
++# Pre-fix: the read loop walked buffer[0..sizeof-2] one byte at a
++# time, then post-loop logic did "if (*cp != '\n') cp++; *cp-- =
++# '\0';". If no newline arrived before the loop filled the buffer,
++# cp was left at &buffer[sizeof-1] (never written by the loop),
++# *cp held stale stack bytes, and cp++ pushed cp one past the array.
++# The null-termination then wrote one byte out of bounds on the
++# stack. AddressSanitizer reports stack-buffer-overflow at the
++# null-termination site.
++#
++# Post-fix: the bound-exhaustion case is detected by position and
++# rejected with an "proxy response line too long" message, so no
++# OOB write occurs and rsync exits with a non-signal status.
++
++. "$suitedir/rsync.fns"
++
++command -v python3 >/dev/null 2>&1 || test_skipped "python3 not available"
++
++workdir="$scratchdir/workdir"
++mkdir -p "$workdir"
++cd "$workdir"
++
++port_file="$workdir/port"
++proxy_log="$workdir/proxy.log"
++
++# A minimal TCP listener: binds to an ephemeral port on 127.0.0.1,
++# writes the chosen port to $port_file *before* accept() so the test
++# can synchronise without a sleep, accepts one connection, reads
++# until end-of-headers or 64 KiB, sends exactly 1023 bytes of 'X'
++# with no '\n', then closes.
++python3 - "$port_file" >"$proxy_log" 2>&1 <<'PYEOF' &
++import socket, sys, os
++port_file = sys.argv[1]
++s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
++s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
++s.bind(("127.0.0.1", 0))
++port = s.getsockname()[1]
++tmp = port_file + ".tmp"
++with open(tmp, "w") as fp:
++ fp.write("%d\n" % port)
++os.rename(tmp, port_file) # atomic visibility to the shell side
++s.listen(1)
++conn, _ = s.accept()
++conn.settimeout(5)
++try:
++ data = b""
++ while b"\r\n\r\n" not in data and len(data) < 65536:
++ chunk = conn.recv(8192)
++ if not chunk:
++ break
++ data += chunk
++except socket.timeout:
++ pass
++conn.sendall(b"X" * 1023) # exactly the buffer-1 trigger size
++try:
++ conn.shutdown(socket.SHUT_RDWR)
++except OSError:
++ pass
++conn.close()
++s.close()
++PYEOF
++proxy_pid=$!
++
++# Wait up to ~10s for the listener to publish its port.
++i=0
++while [ ! -s "$port_file" ] && [ $i -lt 10 ]; do
++ sleep 1
++ i=$((i + 1))
++done
++
++if [ ! -s "$port_file" ]; then
++ kill "$proxy_pid" 2>/dev/null
++ cat "$proxy_log" >&2 2>/dev/null
++ test_fail "proxy listener never published a port"
++fi
++
++port=`cat "$port_file"`
++case "$port" in
++ *[!0-9]*|"") kill "$proxy_pid" 2>/dev/null; test_fail "bogus port from listener: '$port'" ;;
++esac
++
++# Run rsync through the malicious proxy. Any rsync:// URL works:
++# the proxy intercepts the CONNECT and never forwards anywhere.
++rsync_err="$workdir/rsync.err"
++
++# rsync MUST exit non-zero here (the proxy is misbehaving).
++# Use `|| status=$?` so we capture the real exit code under `sh -e`;
++# `if ! cmd; then status=$?` would only ever see 0 because the `!`
++# is the last command before `$?`.
++status=0
++RSYNC_PROXY="127.0.0.1:$port" \
++ $RSYNC rsync://example.invalid:873/whatever/ "$workdir/out/" \
++ >/dev/null 2>"$rsync_err" || status=$?
++
++# Reap the listener.
++wait "$proxy_pid" 2>/dev/null || true
++
++# 1. rsync must not have crashed (SIGSEGV/SIGABRT report >= 128).
++if [ "$status" -ge 128 ]; then
++ cat "$rsync_err" >&2
++ test_fail "rsync killed by signal (status=$status) -- possible stack OOB regression"
++fi
++
++# 2. rsync must have actually exited non-zero (i.e. saw the bad proxy).
++if [ "$status" -eq 0 ]; then
++ cat "$rsync_err" >&2
++ test_fail "rsync returned success despite malformed proxy response"
++fi
++
++# 3. The new error message must appear.
++if ! grep -q "proxy response line too long" "$rsync_err"; then
++ cat "$rsync_err" >&2
++ test_fail "expected 'proxy response line too long' in rsync stderr"
++fi
++
++echo "OK: over-long proxy response line rejected cleanly without crashing"
++exit 0
+--
+2.35.6
+
diff --git a/meta/recipes-devtools/rsync/rsync_3.2.7.bb b/meta/recipes-devtools/rsync/rsync_3.2.7.bb
index a27fb0f291..6c164521fb 100644
--- a/meta/recipes-devtools/rsync/rsync_3.2.7.bb
+++ b/meta/recipes-devtools/rsync/rsync_3.2.7.bb
@@ -41,6 +41,7 @@ SRC_URI = "https://download.samba.org/pub/${BPN}/src/${BP}.tar.gz \
file://CVE-2026-43618.patch \
file://CVE-2026-43620.patch \
file://CVE-2026-43617.patch \
+ file://CVE-2026-45232.patch \
"
SRC_URI[sha256sum] = "4e7d9d3f6ed10878c58c5fb724a67dacf4b6aac7340b13e488fb2dc41346f2bb"
--
2.44.1
^ permalink raw reply related [flat|nested] 15+ messages in thread
* Re: [OE-core][scarthgap][PATCH 1/6] rsync: Fix CVE-2026-29518
2026-06-12 12:13 [OE-core][scarthgap][PATCH 1/6] rsync: Fix CVE-2026-29518 Ashishkumar Parmar X (asparmar - E INFOCHIPS PRIVATE LIMITED at Cisco)
` (4 preceding siblings ...)
2026-06-12 12:13 ` [OE-core][scarthgap][PATCH 6/6] rsync: Fix CVE-2026-45232 Ashishkumar Parmar X (asparmar - E INFOCHIPS PRIVATE LIMITED at Cisco)
@ 2026-06-22 11:31 ` Yoann Congal
2026-07-06 17:19 ` Yoann Congal
2026-07-27 21:34 ` Yoann Congal
2026-07-28 13:16 ` Yoann Congal
7 siblings, 1 reply; 15+ messages in thread
From: Yoann Congal @ 2026-06-22 11:31 UTC (permalink / raw)
To: asparmar, openembedded-core; +Cc: xe-linux-external
On Fri Jun 12, 2026 at 2:13 PM CEST, Ashishkumar Parmar X (asparmar - E INFOCHIPS PRIVATE LIMITED at Cisco) via lists.openembedded.org wrote:
> From: Ashishkumar Parmar <asparmar@cisco.com>
>
> Pick the upstream backport [1] for CVE-2026-29518 as mentioned in [3],
> where a non-chrooted rsync daemon could be exposed to a parent path
> TOCTOU race that allowed file access outside the module.
>
> Also include the dependent upstream fix that followed the CVE fix:
> - CVE-2026-29518_p2.patch [2] secures sender read-path opens by
> opening files from the module root, closing the same race on the
> read side.
>
> [1] https://github.com/RsyncProject/rsync/commit/1a5ad81add1004354a3d8ba841b94ffe19cd2505
> [2] https://github.com/RsyncProject/rsync/commit/99b36291d06ca66229942c7a525a1f5566f10c85
> [3] https://www.cve.org/CVERecord?id=CVE-2026-29518
>
> Signed-off-by: Ashishkumar Parmar <asparmar@cisco.com>
> ---
Hello,
It looks like this whole series is needed for wrynose (rsync 3.4.1).
Can you send fixes there first, and, then, ping here?
Thanks!
> .../rsync/files/CVE-2026-29518_p1.patch | 330 ++++++++++++++++++
> .../rsync/files/CVE-2026-29518_p2.patch | 73 ++++
> meta/recipes-devtools/rsync/rsync_3.2.7.bb | 2 +
> 3 files changed, 405 insertions(+)
> create mode 100644 meta/recipes-devtools/rsync/files/CVE-2026-29518_p1.patch
> create mode 100644 meta/recipes-devtools/rsync/files/CVE-2026-29518_p2.patch
>
> diff --git a/meta/recipes-devtools/rsync/files/CVE-2026-29518_p1.patch b/meta/recipes-devtools/rsync/files/CVE-2026-29518_p1.patch
> new file mode 100644
> index 0000000000..227ca56dd3
> --- /dev/null
> +++ b/meta/recipes-devtools/rsync/files/CVE-2026-29518_p1.patch
> @@ -0,0 +1,330 @@
> +From c5192e125999130b7e15c621989839da31b15a05 Mon Sep 17 00:00:00 2001
> +From: Andrew Tridgell <andrew@tridgell.net>
> +Date: Wed, 31 Dec 2025 10:01:23 +1100
> +Subject: [PATCH] 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) <noreply@anthropic.com>
> +
> +CVE: CVE-2026-29518
> +Upstream-Status: Backport [https://github.com/RsyncProject/rsync/commit/1a5ad81add1004354a3d8ba841b94ffe19cd2505]
> +
> +(cherry picked from commit 1a5ad81add1004354a3d8ba841b94ffe19cd2505)
> +Signed-off-by: Ashishkumar Parmar <asparmar@cisco.com>
> +---
> + 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 7c897abc..b6eba098 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;
> +@@ -981,6 +983,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;
> + }
> +
> +@@ -1003,6 +1006,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])) {
> +@@ -1305,6 +1317,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 d38bbe8d..d4ca5396 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 8aab2cc0..8b39a6e2 100644
> +--- a/syscall.c
> ++++ b/syscall.c
> +@@ -882,6 +882,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.35.6
> diff --git a/meta/recipes-devtools/rsync/files/CVE-2026-29518_p2.patch b/meta/recipes-devtools/rsync/files/CVE-2026-29518_p2.patch
> new file mode 100644
> index 0000000000..4ce3d03248
> --- /dev/null
> +++ b/meta/recipes-devtools/rsync/files/CVE-2026-29518_p2.patch
> @@ -0,0 +1,73 @@
> +From 37d459f837868cf5dc6a3f4962c8c9d9bcd2d4b8 Mon Sep 17 00:00:00 2001
> +From: Andrew Tridgell <andrew@tridgell.net>
> +Date: Sun, 1 Mar 2026 09:28:40 +1100
> +Subject: [PATCH] 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 <noreply@anthropic.com>
> +
> +CVE: CVE-2026-29518
> +Upstream-Status: Backport [https://github.com/RsyncProject/rsync/commit/99b36291d06ca66229942c7a525a1f5566f10c85]
> +
> +(cherry picked from commit 99b36291d06ca66229942c7a525a1f5566f10c85)
> +Signed-off-by: Ashishkumar Parmar <asparmar@cisco.com>
> +---
> + 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.35.6
> diff --git a/meta/recipes-devtools/rsync/rsync_3.2.7.bb b/meta/recipes-devtools/rsync/rsync_3.2.7.bb
> index 2a1c3d9d56..fdbee387e3 100644
> --- a/meta/recipes-devtools/rsync/rsync_3.2.7.bb
> +++ b/meta/recipes-devtools/rsync/rsync_3.2.7.bb
> @@ -29,6 +29,8 @@ SRC_URI = "https://download.samba.org/pub/${BPN}/src/${BP}.tar.gz \
> file://CVE-2024-12747.patch \
> file://CVE-2025-10158.patch \
> file://CVE-2026-41035.patch \
> + file://CVE-2026-29518_p1.patch \
> + file://CVE-2026-29518_p2.patch \
> "
> SRC_URI[sha256sum] = "4e7d9d3f6ed10878c58c5fb724a67dacf4b6aac7340b13e488fb2dc41346f2bb"
>
--
Yoann Congal
Smile ECS
^ permalink raw reply [flat|nested] 15+ messages in thread
* Re: [OE-core][scarthgap][PATCH 1/6] rsync: Fix CVE-2026-29518
2026-06-22 11:31 ` [OE-core][scarthgap][PATCH 1/6] rsync: Fix CVE-2026-29518 Yoann Congal
@ 2026-07-06 17:19 ` Yoann Congal
2026-07-07 9:52 ` Ashishkumar Parmar -X (asparmar - E INFOCHIPS PRIVATE LIMITED at Cisco)
0 siblings, 1 reply; 15+ messages in thread
From: Yoann Congal @ 2026-07-06 17:19 UTC (permalink / raw)
To: Yoann Congal, asparmar, openembedded-core; +Cc: xe-linux-external
On Mon Jun 22, 2026 at 1:31 PM CEST, Yoann Congal wrote:
> On Fri Jun 12, 2026 at 2:13 PM CEST, Ashishkumar Parmar X (asparmar - E INFOCHIPS PRIVATE LIMITED at Cisco) via lists.openembedded.org wrote:
>> From: Ashishkumar Parmar <asparmar@cisco.com>
>>
>> Pick the upstream backport [1] for CVE-2026-29518 as mentioned in [3],
>> where a non-chrooted rsync daemon could be exposed to a parent path
>> TOCTOU race that allowed file access outside the module.
>>
>> Also include the dependent upstream fix that followed the CVE fix:
>> - CVE-2026-29518_p2.patch [2] secures sender read-path opens by
>> opening files from the module root, closing the same race on the
>> read side.
>>
>> [1] https://github.com/RsyncProject/rsync/commit/1a5ad81add1004354a3d8ba841b94ffe19cd2505
>> [2] https://github.com/RsyncProject/rsync/commit/99b36291d06ca66229942c7a525a1f5566f10c85
>> [3] https://www.cve.org/CVERecord?id=CVE-2026-29518
>>
>> Signed-off-by: Ashishkumar Parmar <asparmar@cisco.com>
>> ---
>
> Hello,
>
> It looks like this whole series is needed for wrynose (rsync 3.4.1).
> Can you send fixes there first, and, then, ping here?
Hello Ashishkumar,
Gentle ping for this patch series. Do you plan to send the needed
wrynose series?
Regards,
>
> Thanks!
>
>> .../rsync/files/CVE-2026-29518_p1.patch | 330 ++++++++++++++++++
>> .../rsync/files/CVE-2026-29518_p2.patch | 73 ++++
>> meta/recipes-devtools/rsync/rsync_3.2.7.bb | 2 +
>> 3 files changed, 405 insertions(+)
>> create mode 100644 meta/recipes-devtools/rsync/files/CVE-2026-29518_p1.patch
>> create mode 100644 meta/recipes-devtools/rsync/files/CVE-2026-29518_p2.patch
--
Yoann Congal
Smile ECS
^ permalink raw reply [flat|nested] 15+ messages in thread
* Re: [OE-core][scarthgap][PATCH 1/6] rsync: Fix CVE-2026-29518
2026-07-06 17:19 ` Yoann Congal
@ 2026-07-07 9:52 ` Ashishkumar Parmar -X (asparmar - E INFOCHIPS PRIVATE LIMITED at Cisco)
2026-07-23 9:16 ` Yoann Congal
0 siblings, 1 reply; 15+ messages in thread
From: Ashishkumar Parmar -X (asparmar - E INFOCHIPS PRIVATE LIMITED at Cisco) @ 2026-07-07 9:52 UTC (permalink / raw)
To: Yoann Congal, openembedded-core@lists.openembedded.org
Cc: xe-linux-external (Internal Group)
[-- Attachment #1: Type: text/plain, Size: 2478 bytes --]
Hi Yoann,
Thanks for the reminder.
Yes, I am already working on the required rsync fixes for wrynose first, as suggested, and plan to send the wrynose series in the next 2-3 days.
Once that is posted, I will follow up on this series accordingly.
Regards,
Ashish
________________________________
From: Yoann Congal <yoann.congal@smile.fr>
Sent: 06 July 2026 22:49
To: Yoann Congal <yoann.congal@smile.fr>; Ashishkumar Parmar -X (asparmar - E INFOCHIPS PRIVATE LIMITED at Cisco) <asparmar@cisco.com>; openembedded-core@lists.openembedded.org <openembedded-core@lists.openembedded.org>
Cc: xe-linux-external (Internal Group) <xe-linux-external@cisco.com>
Subject: Re: [OE-core][scarthgap][PATCH 1/6] rsync: Fix CVE-2026-29518
On Mon Jun 22, 2026 at 1:31 PM CEST, Yoann Congal wrote:
> On Fri Jun 12, 2026 at 2:13 PM CEST, Ashishkumar Parmar X (asparmar - E INFOCHIPS PRIVATE LIMITED at Cisco) via lists.openembedded.org wrote:
>> From: Ashishkumar Parmar <asparmar@cisco.com>
>>
>> Pick the upstream backport [1] for CVE-2026-29518 as mentioned in [3],
>> where a non-chrooted rsync daemon could be exposed to a parent path
>> TOCTOU race that allowed file access outside the module.
>>
>> Also include the dependent upstream fix that followed the CVE fix:
>> - CVE-2026-29518_p2.patch [2] secures sender read-path opens by
>> opening files from the module root, closing the same race on the
>> read side.
>>
>> [1] https://github.com/RsyncProject/rsync/commit/1a5ad81add1004354a3d8ba841b94ffe19cd2505
>> [2] https://github.com/RsyncProject/rsync/commit/99b36291d06ca66229942c7a525a1f5566f10c85
>> [3] https://www.cve.org/CVERecord?id=CVE-2026-29518
>>
>> Signed-off-by: Ashishkumar Parmar <asparmar@cisco.com>
>> ---
>
> Hello,
>
> It looks like this whole series is needed for wrynose (rsync 3.4.1).
> Can you send fixes there first, and, then, ping here?
Hello Ashishkumar,
Gentle ping for this patch series. Do you plan to send the needed
wrynose series?
Regards,
>
> Thanks!
>
>> .../rsync/files/CVE-2026-29518_p1.patch | 330 ++++++++++++++++++
>> .../rsync/files/CVE-2026-29518_p2.patch | 73 ++++
>> meta/recipes-devtools/rsync/rsync_3.2.7.bb | 2 +
>> 3 files changed, 405 insertions(+)
>> create mode 100644 meta/recipes-devtools/rsync/files/CVE-2026-29518_p1.patch
>> create mode 100644 meta/recipes-devtools/rsync/files/CVE-2026-29518_p2.patch
--
Yoann Congal
Smile ECS
[-- Attachment #2: Type: text/html, Size: 5621 bytes --]
^ permalink raw reply [flat|nested] 15+ messages in thread
* Re: [OE-core][scarthgap][PATCH 1/6] rsync: Fix CVE-2026-29518
2026-07-07 9:52 ` Ashishkumar Parmar -X (asparmar - E INFOCHIPS PRIVATE LIMITED at Cisco)
@ 2026-07-23 9:16 ` Yoann Congal
2026-07-24 4:49 ` Ashishkumar Parmar -X (asparmar - E INFOCHIPS PRIVATE LIMITED at Cisco)
0 siblings, 1 reply; 15+ messages in thread
From: Yoann Congal @ 2026-07-23 9:16 UTC (permalink / raw)
To: Ashishkumar Parmar -X (asparmar - E INFOCHIPS PRIVATE LIMITED at Cisco),
openembedded-core@lists.openembedded.org
Cc: xe-linux-external (Internal Group)
On Tue Jul 7, 2026 at 11:52 AM CEST, Ashishkumar Parmar -X (asparmar - E INFOCHIPS PRIVATE LIMITED at Cisco) wrote:
> Hi Yoann,
>
> Thanks for the reminder.
>
> Yes, I am already working on the required rsync fixes for wrynose first, as suggested, and plan to send the wrynose series in the next 2-3 days.
>
> Once that is posted, I will follow up on this series accordingly.
>
> Regards,
> Ashish
Hello Ashish,
As a reminder, this series is still blocked pending a v3 wrynose series
(you told me to drop v2):
https://lore.kernel.org/all/20260717052059.773645-1-asparmar@cisco.com/
My plan is to keep checking this for a few weeks but ultimately drop
this series if this situation does not change.
Regards,
> ________________________________
> From: Yoann Congal <yoann.congal@smile.fr>
> Sent: 06 July 2026 22:49
> To: Yoann Congal <yoann.congal@smile.fr>; Ashishkumar Parmar -X (asparmar - E INFOCHIPS PRIVATE LIMITED at Cisco) <asparmar@cisco.com>; openembedded-core@lists.openembedded.org <openembedded-core@lists.openembedded.org>
> Cc: xe-linux-external (Internal Group) <xe-linux-external@cisco.com>
> Subject: Re: [OE-core][scarthgap][PATCH 1/6] rsync: Fix CVE-2026-29518
>
> On Mon Jun 22, 2026 at 1:31 PM CEST, Yoann Congal wrote:
>> On Fri Jun 12, 2026 at 2:13 PM CEST, Ashishkumar Parmar X (asparmar - E INFOCHIPS PRIVATE LIMITED at Cisco) via lists.openembedded.org wrote:
>>> From: Ashishkumar Parmar <asparmar@cisco.com>
>>>
>>> Pick the upstream backport [1] for CVE-2026-29518 as mentioned in [3],
>>> where a non-chrooted rsync daemon could be exposed to a parent path
>>> TOCTOU race that allowed file access outside the module.
>>>
>>> Also include the dependent upstream fix that followed the CVE fix:
>>> - CVE-2026-29518_p2.patch [2] secures sender read-path opens by
>>> opening files from the module root, closing the same race on the
>>> read side.
>>>
>>> [1] https://github.com/RsyncProject/rsync/commit/1a5ad81add1004354a3d8ba841b94ffe19cd2505
>>> [2] https://github.com/RsyncProject/rsync/commit/99b36291d06ca66229942c7a525a1f5566f10c85
>>> [3] https://www.cve.org/CVERecord?id=CVE-2026-29518
>>>
>>> Signed-off-by: Ashishkumar Parmar <asparmar@cisco.com>
>>> ---
>>
>> Hello,
>>
>> It looks like this whole series is needed for wrynose (rsync 3.4.1).
>> Can you send fixes there first, and, then, ping here?
>
> Hello Ashishkumar,
>
> Gentle ping for this patch series. Do you plan to send the needed
> wrynose series?
>
> Regards,
>
>>
>> Thanks!
>>
>>> .../rsync/files/CVE-2026-29518_p1.patch | 330 ++++++++++++++++++
>>> .../rsync/files/CVE-2026-29518_p2.patch | 73 ++++
>>> meta/recipes-devtools/rsync/rsync_3.2.7.bb | 2 +
>>> 3 files changed, 405 insertions(+)
>>> create mode 100644 meta/recipes-devtools/rsync/files/CVE-2026-29518_p1.patch
>>> create mode 100644 meta/recipes-devtools/rsync/files/CVE-2026-29518_p2.patch
>
> --
> Yoann Congal
> Smile ECS
--
Yoann Congal
Smile ECS
^ permalink raw reply [flat|nested] 15+ messages in thread
* Re: [OE-core][scarthgap][PATCH 1/6] rsync: Fix CVE-2026-29518
2026-07-23 9:16 ` Yoann Congal
@ 2026-07-24 4:49 ` Ashishkumar Parmar -X (asparmar - E INFOCHIPS PRIVATE LIMITED at Cisco)
0 siblings, 0 replies; 15+ messages in thread
From: Ashishkumar Parmar -X (asparmar - E INFOCHIPS PRIVATE LIMITED at Cisco) @ 2026-07-24 4:49 UTC (permalink / raw)
To: Yoann Congal, openembedded-core@lists.openembedded.org
Cc: xe-linux-external (Internal Group)
[-- Attachment #1: Type: text/plain, Size: 3717 bytes --]
Hi Yoann,
Thanks for the reminder.
I have now sent the v3 rsync patch series for wrynose. It includes the updates requested during the review of the previous version.
Please let me know if any further changes are needed.
Regards,
Ashish
________________________________
From: Yoann Congal <yoann.congal@smile.fr>
Sent: 23 July 2026 14:46
To: Ashishkumar Parmar -X (asparmar - E INFOCHIPS PRIVATE LIMITED at Cisco) <asparmar@cisco.com>; openembedded-core@lists.openembedded.org <openembedded-core@lists.openembedded.org>
Cc: xe-linux-external (Internal Group) <xe-linux-external@cisco.com>
Subject: Re: [OE-core][scarthgap][PATCH 1/6] rsync: Fix CVE-2026-29518
On Tue Jul 7, 2026 at 11:52 AM CEST, Ashishkumar Parmar -X (asparmar - E INFOCHIPS PRIVATE LIMITED at Cisco) wrote:
> Hi Yoann,
>
> Thanks for the reminder.
>
> Yes, I am already working on the required rsync fixes for wrynose first, as suggested, and plan to send the wrynose series in the next 2-3 days.
>
> Once that is posted, I will follow up on this series accordingly.
>
> Regards,
> Ashish
Hello Ashish,
As a reminder, this series is still blocked pending a v3 wrynose series
(you told me to drop v2):
https://lore.kernel.org/all/20260717052059.773645-1-asparmar@cisco.com/
My plan is to keep checking this for a few weeks but ultimately drop
this series if this situation does not change.
Regards,
> ________________________________
> From: Yoann Congal <yoann.congal@smile.fr>
> Sent: 06 July 2026 22:49
> To: Yoann Congal <yoann.congal@smile.fr>; Ashishkumar Parmar -X (asparmar - E INFOCHIPS PRIVATE LIMITED at Cisco) <asparmar@cisco.com>; openembedded-core@lists.openembedded.org <openembedded-core@lists.openembedded.org>
> Cc: xe-linux-external (Internal Group) <xe-linux-external@cisco.com>
> Subject: Re: [OE-core][scarthgap][PATCH 1/6] rsync: Fix CVE-2026-29518
>
> On Mon Jun 22, 2026 at 1:31 PM CEST, Yoann Congal wrote:
>> On Fri Jun 12, 2026 at 2:13 PM CEST, Ashishkumar Parmar X (asparmar - E INFOCHIPS PRIVATE LIMITED at Cisco) via lists.openembedded.org wrote:
>>> From: Ashishkumar Parmar <asparmar@cisco.com>
>>>
>>> Pick the upstream backport [1] for CVE-2026-29518 as mentioned in [3],
>>> where a non-chrooted rsync daemon could be exposed to a parent path
>>> TOCTOU race that allowed file access outside the module.
>>>
>>> Also include the dependent upstream fix that followed the CVE fix:
>>> - CVE-2026-29518_p2.patch [2] secures sender read-path opens by
>>> opening files from the module root, closing the same race on the
>>> read side.
>>>
>>> [1] https://github.com/RsyncProject/rsync/commit/1a5ad81add1004354a3d8ba841b94ffe19cd2505
>>> [2] https://github.com/RsyncProject/rsync/commit/99b36291d06ca66229942c7a525a1f5566f10c85
>>> [3] https://www.cve.org/CVERecord?id=CVE-2026-29518
>>>
>>> Signed-off-by: Ashishkumar Parmar <asparmar@cisco.com>
>>> ---
>>
>> Hello,
>>
>> It looks like this whole series is needed for wrynose (rsync 3.4.1).
>> Can you send fixes there first, and, then, ping here?
>
> Hello Ashishkumar,
>
> Gentle ping for this patch series. Do you plan to send the needed
> wrynose series?
>
> Regards,
>
>>
>> Thanks!
>>
>>> .../rsync/files/CVE-2026-29518_p1.patch | 330 ++++++++++++++++++
>>> .../rsync/files/CVE-2026-29518_p2.patch | 73 ++++
>>> meta/recipes-devtools/rsync/rsync_3.2.7.bb | 2 +
>>> 3 files changed, 405 insertions(+)
>>> create mode 100644 meta/recipes-devtools/rsync/files/CVE-2026-29518_p1.patch
>>> create mode 100644 meta/recipes-devtools/rsync/files/CVE-2026-29518_p2.patch
>
> --
> Yoann Congal
> Smile ECS
--
Yoann Congal
Smile ECS
[-- Attachment #2: Type: text/html, Size: 7320 bytes --]
^ permalink raw reply [flat|nested] 15+ messages in thread
* Re: [OE-core][scarthgap][PATCH 1/6] rsync: Fix CVE-2026-29518
2026-06-12 12:13 [OE-core][scarthgap][PATCH 1/6] rsync: Fix CVE-2026-29518 Ashishkumar Parmar X (asparmar - E INFOCHIPS PRIVATE LIMITED at Cisco)
` (5 preceding siblings ...)
2026-06-22 11:31 ` [OE-core][scarthgap][PATCH 1/6] rsync: Fix CVE-2026-29518 Yoann Congal
@ 2026-07-27 21:34 ` Yoann Congal
2026-07-28 13:16 ` Yoann Congal
7 siblings, 0 replies; 15+ messages in thread
From: Yoann Congal @ 2026-07-27 21:34 UTC (permalink / raw)
To: asparmar, openembedded-core; +Cc: xe-linux-external
On Fri Jun 12, 2026 at 2:13 PM CEST, Ashishkumar Parmar X (asparmar - E INFOCHIPS PRIVATE LIMITED at Cisco) via lists.openembedded.org wrote:
> From: Ashishkumar Parmar <asparmar@cisco.com>
>
> Pick the upstream backport [1] for CVE-2026-29518 as mentioned in [3],
> where a non-chrooted rsync daemon could be exposed to a parent path
> TOCTOU race that allowed file access outside the module.
>
> Also include the dependent upstream fix that followed the CVE fix:
> - CVE-2026-29518_p2.patch [2] secures sender read-path opens by
> opening files from the module root, closing the same race on the
> read side.
>
> [1] https://github.com/RsyncProject/rsync/commit/1a5ad81add1004354a3d8ba841b94ffe19cd2505
> [2] https://github.com/RsyncProject/rsync/commit/99b36291d06ca66229942c7a525a1f5566f10c85
> [3] https://www.cve.org/CVERecord?id=CVE-2026-29518
>
> Signed-off-by: Ashishkumar Parmar <asparmar@cisco.com>
> ---
> .../rsync/files/CVE-2026-29518_p1.patch | 330 ++++++++++++++++++
> .../rsync/files/CVE-2026-29518_p2.patch | 73 ++++
> meta/recipes-devtools/rsync/rsync_3.2.7.bb | 2 +
> 3 files changed, 405 insertions(+)
> create mode 100644 meta/recipes-devtools/rsync/files/CVE-2026-29518_p1.patch
> create mode 100644 meta/recipes-devtools/rsync/files/CVE-2026-29518_p2.patch
The backported commit[1] is from a branch called "v3.2.7-sec-patches" with
a tag "v3.2.7-sec-patches1".
If upstream has a branch looking like a stable branch, shouldn't we try
to upgrade on this branch?
Can you look if a "v3.2.7 -> v3.2.7-sec-patches1 upgrade" would be
possible on scarthgap?
Thanks,
--
Yoann Congal
Smile ECS
^ permalink raw reply [flat|nested] 15+ messages in thread
* Re: [OE-core][scarthgap][PATCH 1/6] rsync: Fix CVE-2026-29518
2026-06-12 12:13 [OE-core][scarthgap][PATCH 1/6] rsync: Fix CVE-2026-29518 Ashishkumar Parmar X (asparmar - E INFOCHIPS PRIVATE LIMITED at Cisco)
` (6 preceding siblings ...)
2026-07-27 21:34 ` Yoann Congal
@ 2026-07-28 13:16 ` Yoann Congal
2026-07-28 13:50 ` Ashishkumar Parmar -X (asparmar - E INFOCHIPS PRIVATE LIMITED at Cisco)
7 siblings, 1 reply; 15+ messages in thread
From: Yoann Congal @ 2026-07-28 13:16 UTC (permalink / raw)
To: asparmar, openembedded-core; +Cc: xe-linux-external
On Fri Jun 12, 2026 at 2:13 PM CEST, Ashishkumar Parmar X (asparmar - E INFOCHIPS PRIVATE LIMITED at Cisco) via lists.openembedded.org wrote:
> From: Ashishkumar Parmar <asparmar@cisco.com>
>
> Pick the upstream backport [1] for CVE-2026-29518 as mentioned in [3],
> where a non-chrooted rsync daemon could be exposed to a parent path
> TOCTOU race that allowed file access outside the module.
>
> Also include the dependent upstream fix that followed the CVE fix:
> - CVE-2026-29518_p2.patch [2] secures sender read-path opens by
> opening files from the module root, closing the same race on the
> read side.
>
> [1] https://github.com/RsyncProject/rsync/commit/1a5ad81add1004354a3d8ba841b94ffe19cd2505
> [2] https://github.com/RsyncProject/rsync/commit/99b36291d06ca66229942c7a525a1f5566f10c85
> [3] https://www.cve.org/CVERecord?id=CVE-2026-29518
>
> Signed-off-by: Ashishkumar Parmar <asparmar@cisco.com>
> ---
> .../rsync/files/CVE-2026-29518_p1.patch | 330 ++++++++++++++++++
> .../rsync/files/CVE-2026-29518_p2.patch | 73 ++++
> meta/recipes-devtools/rsync/rsync_3.2.7.bb | 2 +
> 3 files changed, 405 insertions(+)
> create mode 100644 meta/recipes-devtools/rsync/files/CVE-2026-29518_p1.patch
> create mode 100644 meta/recipes-devtools/rsync/files/CVE-2026-29518_p2.patch
Hello,
Something in this series made the test fail on debian 11:
https://autobuilder.yoctoproject.org/valkyrie/?#/builders/35/builds/4395/steps/15/logs/stdio
| 399 ERROR: rsync-native-3.2.7-r0 do_compile: oe_runmake failed
| 400 ERROR: rsync-native-3.2.7-r0 do_compile: ExecutionError('/srv/pokybuild/yocto-worker/oe-selftest-debian/build/build-st-2690503/tmp/work/x86_64-linux/rsync-native/3.2.7/temp/run.do_compile.2977888', 1, None, None)
| 401 NOTE: recipe gcc-source-13.4.0-13.4.0-r0: task do_patch: Succeeded
| 402 NOTE: Running task 392 of 412 (/srv/pokybuild/yocto-worker/oe-selftest-debian/build/meta/recipes-devtools/gcc/gcc-source_13.4.bb:do_preconfigure)
| 403 NOTE: recipe gcc-source-13.4.0-13.4.0-r0: task do_preconfigure: Started
| 404 NOTE: recipe gcc-source-13.4.0-13.4.0-r0: task do_preconfigure: Succeeded
| 405 ERROR: Logfile of failure stored in: /srv/pokybuild/yocto-worker/oe-selftest-debian/build/build-st-2690503/tmp/work/x86_64-linux/rsync-native/3.2.7/temp/log.do_compile.2977888
| 406 Log data follows:
| 407 | DEBUG: Executing python function autotools_aclocals
| 408 | DEBUG: SITE files ['endian-little', 'common-linux', 'common-glibc', 'bit-64', 'x86_64-linux', 'common']
| 409 | DEBUG: Python function autotools_aclocals finished
| 410 | DEBUG: Executing shell function do_compile
| 411 | NOTE: make -j 16 -l 75 STRIP=
| 412 | gawk -f ./daemon-parm.awk ./daemon-parm.txt
| 413 | sed 's;\@bindir\@;/srv/pokybuild/yocto-worker/oe-selftest-debian/build/build-st-2690503/tmp/work/x86_64-linux/rsync-native/3.2.7/recipe-sysroot-native/usr/bin;g' <./stunnel-rsyncd.conf.in >stunnel-rsyncd.conf
| 414 | gawk -f ./mkproto.awk ./*.c ./lib/compat.c daemon-parm.h
| 415 | gawk -f ./define-from-md.awk -v hfile=default-dont-compress.h ./rsync.1.md
| 416 | gawk -f ./define-from-md.awk -v hfile=default-cvsignore.h ./rsync.1.md
| 417 | gcc -I. -I. -I./zlib -isystem/srv/pokybuild/yocto-worker/oe-selftest-debian/build/build-st-2690503/tmp/work/x86_64-linux/rsync-native/3.2.7/recipe-sysroot-native/usr/include -O2 -pipe -DHAVE_CONFIG_H -Wall -W -isystem/srv/pokybuild/yocto-worker/oe-selftest-debian/build/build-st-2690503/tmp/work/x86_64-linux/rsync-native/3.2.7/recipe-sysroot-native/usr/include -c match.c -o match.o
| 418 | gcc -I. -I. -I./zlib -isystem/srv/pokybuild/yocto-worker/oe-selftest-debian/build/build-st-2690503/tmp/work/x86_64-linux/rsync-native/3.2.7/recipe-sysroot-native/usr/include -O2 -pipe -DHAVE_CONFIG_H -Wall -W -isystem/srv/pokybuild/yocto-worker/oe-selftest-debian/build/build-st-2690503/tmp/work/x86_64-linux/rsync-native/3.2.7/recipe-sysroot-native/usr/include -c syscall.c -o syscall.o
| 419 | syscall.c: In function 'secure_relative_open_linux':
| 420 | syscall.c:1723:19: error: 'SYS_openat2' undeclared (first use in this function); did you mean 'SYS_openat'?
| 421 | 1723 | dirfd = syscall(SYS_openat2, AT_FDCWD, basedir, &bhow, sizeof bhow);
| 422 | | ^~~~~~~~~~~
| 423 | | SYS_openat
| 424 | syscall.c:1723:19: note: each undeclared identifier is reported only once for each function it appears in
| 425 | make: *** [Makefile:76: syscall.o] Error 1
| 426 | ERROR: oe_runmake failed
| 427 | WARNING: exit code 1 from a shell command.
| 428 NOTE: recipe rsync-native-3.2.7-r0: task do_compile: Failed
| 429 ERROR: Task (virtual:native:/srv/pokybuild/yocto-worker/oe-selftest-debian/build/meta/recipes-devtools/rsync/rsync_3.2.7.bb:do_compile) failed with exit code '1'
| 430 NOTE: recipe linux-libc-headers-6.6-r0: task do_unpack: Succeeded
| 431 NOTE: Tasks Summary: Attempted 394 tasks of which 381 didn't need to be rerun and 1 failed.
| 432 NOTE: The errors for this build are stored in /srv/pokybuild/yocto-worker/oe-selftest-debian/build/build-st-2690503/tmp/log/error-report/error_report_20260728100237.txt
| 433 You can send the errors to a reports server by running:
| 434 send-error-report /srv/pokybuild/yocto-worker/oe-selftest-debian/build/build-st-2690503/tmp/log/error-report/error_report_20260728100237.txt [-s server]
| 435 NOTE: The contents of these logs will be posted in public if you use the above command with the default server. Please ensure you remove any identifying or proprietary information when prompted before sending.
| 436
| 437 Summary: 1 task failed:
| 438 virtual:native:/srv/pokybuild/yocto-worker/oe-selftest-debian/build/meta/recipes-devtools/rsync/rsync_3.2.7.bb:do_compile
| 439 log: /srv/pokybuild/yocto-worker/oe-selftest-debian/build/build-st-2690503/tmp/work/x86_64-linux/rsync-native/3.2.7/temp/log.do_compile.2977888
| 440 Summary: There was 1 WARNING message.
| 441 Summary: There were 2 ERROR messages, returning a non-zero exit code.
Can you look into this? My guess is the newly introduced openat2 syscall
is not compatible with debian11.
FYI, same goes for the wrynose series, I'll answer there.
Regards,
--
Yoann Congal
Smile ECS
^ permalink raw reply [flat|nested] 15+ messages in thread
* Re: [OE-core][scarthgap][PATCH 1/6] rsync: Fix CVE-2026-29518
2026-07-28 13:16 ` Yoann Congal
@ 2026-07-28 13:50 ` Ashishkumar Parmar -X (asparmar - E INFOCHIPS PRIVATE LIMITED at Cisco)
2026-07-28 14:00 ` Yoann Congal
0 siblings, 1 reply; 15+ messages in thread
From: Ashishkumar Parmar -X (asparmar - E INFOCHIPS PRIVATE LIMITED at Cisco) @ 2026-07-28 13:50 UTC (permalink / raw)
To: Yoann Congal, openembedded-core@lists.openembedded.org
Cc: xe-linux-external (Internal Group)
[-- Attachment #1: Type: text/plain, Size: 7962 bytes --]
Hi Yoann,
Thanks for reporting this.
I checked the Debian 11 build failure. The Scarthgap rsync changes currently include <linux/openat2.h> and use SYS_openat2 unconditionally. The Debian 11 build headers do not define SYS_openat2, so the compilation fails.
My local validation passed because the available build headers define SYS_openat2, so it did not cover this older-host case.
I also checked whether upgrading to the upstream v3.2.7-sec-patches1 tag would resolve the problem. It would not: that tag has the same unconditional SYS_openat2 use and does not contain the required configure-time portability check.
The current upstream v3.2.7-sec-patches branch includes the follow-up commit:
https://github.com/RsyncProject/rsync/commit/729ec83612d43e361ada1870173b525bc0b9ebac
This checks for both <linux/openat2.h> and SYS_openat2 during configure, defines HAVE_OPENAT2 only when both are available, and retains the existing fallback on older systems.
Once you confirm,
I will add this follow-up to the Scarthgap series and rerun both rsync-native and rsync build validation before sending a v2.
Thanks,
Ashish
________________________________
From: Yoann Congal <yoann.congal@smile.fr>
Sent: 28 July 2026 18:46
To: Ashishkumar Parmar -X (asparmar - E INFOCHIPS PRIVATE LIMITED at Cisco) <asparmar@cisco.com>; openembedded-core@lists.openembedded.org <openembedded-core@lists.openembedded.org>
Cc: xe-linux-external (Internal Group) <xe-linux-external@cisco.com>
Subject: Re: [OE-core][scarthgap][PATCH 1/6] rsync: Fix CVE-2026-29518
On Fri Jun 12, 2026 at 2:13 PM CEST, Ashishkumar Parmar X (asparmar - E INFOCHIPS PRIVATE LIMITED at Cisco) via lists.openembedded.org wrote:
> From: Ashishkumar Parmar <asparmar@cisco.com>
>
> Pick the upstream backport [1] for CVE-2026-29518 as mentioned in [3],
> where a non-chrooted rsync daemon could be exposed to a parent path
> TOCTOU race that allowed file access outside the module.
>
> Also include the dependent upstream fix that followed the CVE fix:
> - CVE-2026-29518_p2.patch [2] secures sender read-path opens by
> opening files from the module root, closing the same race on the
> read side.
>
> [1] https://github.com/RsyncProject/rsync/commit/1a5ad81add1004354a3d8ba841b94ffe19cd2505
> [2] https://github.com/RsyncProject/rsync/commit/99b36291d06ca66229942c7a525a1f5566f10c85
> [3] https://www.cve.org/CVERecord?id=CVE-2026-29518
>
> Signed-off-by: Ashishkumar Parmar <asparmar@cisco.com>
> ---
> .../rsync/files/CVE-2026-29518_p1.patch | 330 ++++++++++++++++++
> .../rsync/files/CVE-2026-29518_p2.patch | 73 ++++
> meta/recipes-devtools/rsync/rsync_3.2.7.bb | 2 +
> 3 files changed, 405 insertions(+)
> create mode 100644 meta/recipes-devtools/rsync/files/CVE-2026-29518_p1.patch
> create mode 100644 meta/recipes-devtools/rsync/files/CVE-2026-29518_p2.patch
Hello,
Something in this series made the test fail on debian 11:
https://autobuilder.yoctoproject.org/valkyrie/?#/builders/35/builds/4395/steps/15/logs/stdio
| 399 ERROR: rsync-native-3.2.7-r0 do_compile: oe_runmake failed
| 400 ERROR: rsync-native-3.2.7-r0 do_compile: ExecutionError('/srv/pokybuild/yocto-worker/oe-selftest-debian/build/build-st-2690503/tmp/work/x86_64-linux/rsync-native/3.2.7/temp/run.do_compile.2977888', 1, None, None)
| 401 NOTE: recipe gcc-source-13.4.0-13.4.0-r0: task do_patch: Succeeded
| 402 NOTE: Running task 392 of 412 (/srv/pokybuild/yocto-worker/oe-selftest-debian/build/meta/recipes-devtools/gcc/gcc-source_13.4.bb:do_preconfigure)
| 403 NOTE: recipe gcc-source-13.4.0-13.4.0-r0: task do_preconfigure: Started
| 404 NOTE: recipe gcc-source-13.4.0-13.4.0-r0: task do_preconfigure: Succeeded
| 405 ERROR: Logfile of failure stored in: /srv/pokybuild/yocto-worker/oe-selftest-debian/build/build-st-2690503/tmp/work/x86_64-linux/rsync-native/3.2.7/temp/log.do_compile.2977888
| 406 Log data follows:
| 407 | DEBUG: Executing python function autotools_aclocals
| 408 | DEBUG: SITE files ['endian-little', 'common-linux', 'common-glibc', 'bit-64', 'x86_64-linux', 'common']
| 409 | DEBUG: Python function autotools_aclocals finished
| 410 | DEBUG: Executing shell function do_compile
| 411 | NOTE: make -j 16 -l 75 STRIP=
| 412 | gawk -f ./daemon-parm.awk ./daemon-parm.txt
| 413 | sed 's;\@bindir\@;/srv/pokybuild/yocto-worker/oe-selftest-debian/build/build-st-2690503/tmp/work/x86_64-linux/rsync-native/3.2.7/recipe-sysroot-native/usr/bin;g' <./stunnel-rsyncd.conf.in >stunnel-rsyncd.conf
| 414 | gawk -f ./mkproto.awk ./*.c ./lib/compat.c daemon-parm.h
| 415 | gawk -f ./define-from-md.awk -v hfile=default-dont-compress.h ./rsync.1.md
| 416 | gawk -f ./define-from-md.awk -v hfile=default-cvsignore.h ./rsync.1.md
| 417 | gcc -I. -I. -I./zlib -isystem/srv/pokybuild/yocto-worker/oe-selftest-debian/build/build-st-2690503/tmp/work/x86_64-linux/rsync-native/3.2.7/recipe-sysroot-native/usr/include -O2 -pipe -DHAVE_CONFIG_H -Wall -W -isystem/srv/pokybuild/yocto-worker/oe-selftest-debian/build/build-st-2690503/tmp/work/x86_64-linux/rsync-native/3.2.7/recipe-sysroot-native/usr/include -c match.c -o match.o
| 418 | gcc -I. -I. -I./zlib -isystem/srv/pokybuild/yocto-worker/oe-selftest-debian/build/build-st-2690503/tmp/work/x86_64-linux/rsync-native/3.2.7/recipe-sysroot-native/usr/include -O2 -pipe -DHAVE_CONFIG_H -Wall -W -isystem/srv/pokybuild/yocto-worker/oe-selftest-debian/build/build-st-2690503/tmp/work/x86_64-linux/rsync-native/3.2.7/recipe-sysroot-native/usr/include -c syscall.c -o syscall.o
| 419 | syscall.c: In function 'secure_relative_open_linux':
| 420 | syscall.c:1723:19: error: 'SYS_openat2' undeclared (first use in this function); did you mean 'SYS_openat'?
| 421 | 1723 | dirfd = syscall(SYS_openat2, AT_FDCWD, basedir, &bhow, sizeof bhow);
| 422 | | ^~~~~~~~~~~
| 423 | | SYS_openat
| 424 | syscall.c:1723:19: note: each undeclared identifier is reported only once for each function it appears in
| 425 | make: *** [Makefile:76: syscall.o] Error 1
| 426 | ERROR: oe_runmake failed
| 427 | WARNING: exit code 1 from a shell command.
| 428 NOTE: recipe rsync-native-3.2.7-r0: task do_compile: Failed
| 429 ERROR: Task (virtual:native:/srv/pokybuild/yocto-worker/oe-selftest-debian/build/meta/recipes-devtools/rsync/rsync_3.2.7.bb:do_compile) failed with exit code '1'
| 430 NOTE: recipe linux-libc-headers-6.6-r0: task do_unpack: Succeeded
| 431 NOTE: Tasks Summary: Attempted 394 tasks of which 381 didn't need to be rerun and 1 failed.
| 432 NOTE: The errors for this build are stored in /srv/pokybuild/yocto-worker/oe-selftest-debian/build/build-st-2690503/tmp/log/error-report/error_report_20260728100237.txt
| 433 You can send the errors to a reports server by running:
| 434 send-error-report /srv/pokybuild/yocto-worker/oe-selftest-debian/build/build-st-2690503/tmp/log/error-report/error_report_20260728100237.txt [-s server]
| 435 NOTE: The contents of these logs will be posted in public if you use the above command with the default server. Please ensure you remove any identifying or proprietary information when prompted before sending.
| 436
| 437 Summary: 1 task failed:
| 438 virtual:native:/srv/pokybuild/yocto-worker/oe-selftest-debian/build/meta/recipes-devtools/rsync/rsync_3.2.7.bb:do_compile
| 439 log: /srv/pokybuild/yocto-worker/oe-selftest-debian/build/build-st-2690503/tmp/work/x86_64-linux/rsync-native/3.2.7/temp/log.do_compile.2977888
| 440 Summary: There was 1 WARNING message.
| 441 Summary: There were 2 ERROR messages, returning a non-zero exit code.
Can you look into this? My guess is the newly introduced openat2 syscall
is not compatible with debian11.
FYI, same goes for the wrynose series, I'll answer there.
Regards,
--
Yoann Congal
Smile ECS
[-- Attachment #2: Type: text/html, Size: 13886 bytes --]
^ permalink raw reply [flat|nested] 15+ messages in thread
* Re: [OE-core][scarthgap][PATCH 1/6] rsync: Fix CVE-2026-29518
2026-07-28 13:50 ` Ashishkumar Parmar -X (asparmar - E INFOCHIPS PRIVATE LIMITED at Cisco)
@ 2026-07-28 14:00 ` Yoann Congal
0 siblings, 0 replies; 15+ messages in thread
From: Yoann Congal @ 2026-07-28 14:00 UTC (permalink / raw)
To: Ashishkumar Parmar -X (asparmar - E INFOCHIPS PRIVATE LIMITED at Cisco),
openembedded-core@lists.openembedded.org
Cc: xe-linux-external (Internal Group)
On Tue Jul 28, 2026 at 3:50 PM CEST, Ashishkumar Parmar -X (asparmar - E INFOCHIPS PRIVATE LIMITED at Cisco) wrote:
> Hi Yoann,
>
> Thanks for reporting this.
>
> I checked the Debian 11 build failure. The Scarthgap rsync changes currently include <linux/openat2.h> and use SYS_openat2 unconditionally. The Debian 11 build headers do not define SYS_openat2, so the compilation fails.
>
> My local validation passed because the available build headers define SYS_openat2, so it did not cover this older-host case.
>
> I also checked whether upgrading to the upstream v3.2.7-sec-patches1 tag would resolve the problem. It would not: that tag has the same unconditional SYS_openat2 use and does not contain the required configure-time portability check.
>
> The current upstream v3.2.7-sec-patches branch includes the follow-up commit:
> https://github.com/RsyncProject/rsync/commit/729ec83612d43e361ada1870173b525bc0b9ebac
>
> This checks for both <linux/openat2.h> and SYS_openat2 during configure, defines HAVE_OPENAT2 only when both are available, and retains the existing fallback on older systems.
Sounds good.
>
> Once you confirm,
> I will add this follow-up to the Scarthgap series and rerun both rsync-native and rsync build validation before sending a v2.
Please also test debian11 (I used a quick container to reproduce)
Thanks!
--
Yoann Congal
Smile ECS
^ permalink raw reply [flat|nested] 15+ messages in thread
end of thread, other threads:[~2026-07-28 14:00 UTC | newest]
Thread overview: 15+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-06-12 12:13 [OE-core][scarthgap][PATCH 1/6] rsync: Fix CVE-2026-29518 Ashishkumar Parmar X (asparmar - E INFOCHIPS PRIVATE LIMITED at Cisco)
2026-06-12 12:13 ` [OE-core][scarthgap][PATCH 2/6] rsync: Fix CVE-2026-43619 Ashishkumar Parmar X (asparmar - E INFOCHIPS PRIVATE LIMITED at Cisco)
2026-06-12 12:13 ` [OE-core][scarthgap][PATCH 3/6] rsync: Fix CVE-2026-43618 Ashishkumar Parmar X (asparmar - E INFOCHIPS PRIVATE LIMITED at Cisco)
2026-06-12 12:13 ` [OE-core][scarthgap][PATCH 4/6] rsync: Fix CVE-2026-43620 Ashishkumar Parmar X (asparmar - E INFOCHIPS PRIVATE LIMITED at Cisco)
2026-06-12 12:13 ` [OE-core][scarthgap][PATCH 5/6] rsync: Fix CVE-2026-43617 Ashishkumar Parmar X (asparmar - E INFOCHIPS PRIVATE LIMITED at Cisco)
2026-06-12 12:13 ` [OE-core][scarthgap][PATCH 6/6] rsync: Fix CVE-2026-45232 Ashishkumar Parmar X (asparmar - E INFOCHIPS PRIVATE LIMITED at Cisco)
2026-06-22 11:31 ` [OE-core][scarthgap][PATCH 1/6] rsync: Fix CVE-2026-29518 Yoann Congal
2026-07-06 17:19 ` Yoann Congal
2026-07-07 9:52 ` Ashishkumar Parmar -X (asparmar - E INFOCHIPS PRIVATE LIMITED at Cisco)
2026-07-23 9:16 ` Yoann Congal
2026-07-24 4:49 ` Ashishkumar Parmar -X (asparmar - E INFOCHIPS PRIVATE LIMITED at Cisco)
2026-07-27 21:34 ` Yoann Congal
2026-07-28 13:16 ` Yoann Congal
2026-07-28 13:50 ` Ashishkumar Parmar -X (asparmar - E INFOCHIPS PRIVATE LIMITED at Cisco)
2026-07-28 14:00 ` Yoann Congal
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox