Openembedded Core Discussions
 help / color / mirror / Atom feed
* [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)
                   ` (5 more replies)
  0 siblings, 6 replies; 9+ 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] 9+ 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)
                   ` (4 subsequent siblings)
  5 siblings, 0 replies; 9+ 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] 9+ 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)
                   ` (3 subsequent siblings)
  5 siblings, 0 replies; 9+ 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] 9+ 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)
                   ` (2 subsequent siblings)
  5 siblings, 0 replies; 9+ 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] 9+ 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)
  2026-06-22 11:31 ` [OE-core][scarthgap][PATCH 1/6] rsync: Fix CVE-2026-29518 Yoann Congal
  5 siblings, 0 replies; 9+ 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] 9+ 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
  5 siblings, 0 replies; 9+ 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] 9+ 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
  5 siblings, 1 reply; 9+ 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] 9+ 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; 9+ 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] 9+ 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)
  0 siblings, 0 replies; 9+ 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] 9+ messages in thread

end of thread, other threads:[~2026-07-07  9:53 UTC | newest]

Thread overview: 9+ 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)

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox