From patchwork Fri Jun 12 12:13:29 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: "Ashishkumar Parmar X (asparmar - E INFOCHIPS PRIVATE LIMITED at Cisco)" X-Patchwork-Id: 89910 Return-Path: X-Spam-Checker-Version: SpamAssassin 3.4.0 (2014-02-07) on aws-us-west-2-korg-lkml-1.web.codeaurora.org Received: from aws-us-west-2-korg-lkml-1.web.codeaurora.org (localhost.localdomain [127.0.0.1]) by smtp.lore.kernel.org (Postfix) with ESMTP id 3C350CD8CA8 for ; Fri, 12 Jun 2026 12:15:30 +0000 (UTC) Received: from rcdn-iport-6.cisco.com (rcdn-iport-6.cisco.com [173.37.86.77]) by mx.groups.io with SMTP id smtpd.msgproc02-g2.69132.1781266523103137882 for ; Fri, 12 Jun 2026 05:15:23 -0700 Authentication-Results: mx.groups.io; dkim=fail reason="dkim: message contains an insecure body length tag" header.i=@cisco.com header.s=iport01 header.b=IEhevZtt; spf=pass (domain: cisco.com, ip: 173.37.86.77, mailfrom: asparmar@cisco.com) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=cisco.com; i=@cisco.com; l=16958; q=dns/txt; s=iport01; t=1781266523; x=1782476123; h=from:to:cc:subject:date:message-id:mime-version: content-transfer-encoding; bh=3M2CKGK8Jwg+wFlZqSve07XpkbO5lAFnYEW3nAbpOO4=; b=IEhevZttdQGvaN2LR2x96O9ZDY9+BWF9RUVZEKY8xlqAqKWU7eCm9k5A YmVRx9VrsnxYpZYIn2HrEmQGJKXpg4V+Esd1/M6fq+bHUTw0p5dMVgNvy b3AchqDUn6Mx/m5R8QQAP0UFAx4avslAkLrfzIIrLFbIeucRd7/hwyfN4 jEFvefmgf3xsKwZJUzC9Z7zK25QUVvsdgtEHqFuOM4g6EImxWZsSi96gl RNSyeUjqsWAS+6yuhWCl9tCxCutJIHhLUDEXNUqbELqjpQSsfsRbVf8rB lBIfr24WOYz3wzT1/29RysXEooLX0zabupLDq2pjv4ZuqGDn5fIqZ7W6T g==; X-CSE-ConnectionGUID: 4qTfFN6PQGqkqFJy/mRkVg== X-CSE-MsgGUID: mBsIkyBPR6io2wZ8fhmYeQ== X-IPAS-Result: A0AmAADn9itq/5D/Ja1aHQEBAQEJARIBBQUBgXwIAQsBglZ0X0JJjHOJWIEWnQiBfg8BAQEPRA0EAQGFBo1CAiY0CQ4BAgQDAgMBAQEBAQEBAQEBAQsBAQUBAQECAQcFgQ4Thk8NhloBAgEqCwEYAS0sAwECWiMhgwIBgnMCARGyPRo3gXkzgQGDKAE/AkNQ2ywBCxQBBYEzAYU+iB9bGAGEfCcbG4FygRWBO4E4doEFgVwCAgEXgQ2GfgSCInoSgVsCHo5rSIEeA1ksAVUTDQoLBwWBZgM1EioVbjIdgSM+F4EMGwcFgUqBK2qBA4UNIx8DOX+BdIEoZ2kVMDWBAQEREgMLGA1IESw3FBsEPm4HjEIXD4I+ASxHBxMBExh/EggqFCEvU5JgCQE4kgaBNZ9aCiiDdYwhj0KFeBozhASUF5JRC5h9jgqVNIEchGiBaDyBRA4HcBWDIglKGQ+OCx8DCwuDYIUTwnwkNQIJMgEBBwIHDgMLgWiQAAImB4FOAQE IronPort-Data: A9a23:EpCb3qqmQBF313IZ2Y70klss091eBmJJZBIvgKrLsJaIsI4StFCzt garIBmOaa6KYmfyKN4laY3n/B5Qv5HRm4RrT1dlrXhhECxD+ePIVI+TRqvS04x+DSFioGZPt Zh2hgzodZhsJpPkjk7zdOCn9j8kif3gqoPUUIbsIjp2SRJvVBAvgBdin/9RqoNziLBVOSvV0 T/Ji5OZYgPNNwJcaDpOtfrd8Uk35ZwehRtB1rAATaET1LPhvyF94KI3fcmZM3b+S49IKe+2L 86r5K255G7Q4yA2AdqjlLvhGmVSKlIFFVHT4pb+c/HKbilq/kTe4I5iXBYvQRs/ZwGyojxE4 I4lWapc5useFvakdOw1C3G0GszlVEFM0OevzXOX6aR/w6BaGpfh660GMa04AWEX0rpuPW9g0 6IgER8UbQmtpfPxxYi4dOY506zPLOGzVG8ekmtrwTecCbMtRorOBv2Vo9RZxzw3wMtJGJ4yZ eJANmEpN0uGOUASfA5LVPrSn8/w7pX7WzRDsFuPoKMty2PS1wd2lrPqNbI5f/TWFJ4KzxbD+ z+uE2LRBzsBFd+O4wK/rHOr3PD9zS37XbMfLejtnhJtqBjJroAJMzURTVa9rPyzh0KyVt4aI EsO9wIqrLMu7wqsVtT7UhiyrXKIsxJaXMBfe9DW8ymXwabSpgLcDW8eQ3sZN5ottdQ9Qnoh0 Vrhc87VOAGDeYa9ERq1nop4ZxvrUcTJBQfuvRM5cDY= IronPort-HdrOrdr: A9a23:pVxP1KAeAijm0m7lHemA55DYdb4zR+YMi2TDGXofdfUzSL38qy nAppUmPHPP5Qr5O0tQ++xoWpPhfZq0z/cciuMs1NyZMjUO1lHFEGhK1/qH/9SZIVycysdtkY F9bqN5FNr8SXJ+jcr8/U2ENuxI+qjhzEht7t2utkuEimpRGsdd0zs= X-Talos-CUID: 9a23:0Wp7d2O4OK46De5DBDBb+mAyRPgcXWTe1VnyMgi9M2VXR+jA X-Talos-MUID: 9a23:qZiTMQj5FJ1/pTh1gw/OwMMpMe1zwqe/Ing3z5QB4da2ZSJwPgaQtWHi X-IronPort-Anti-Spam-Filtered: true X-IronPort-AV: E=Sophos;i="6.24,200,1774310400"; d="scan'208";a="493798355" Received: from rcdn-l-core-07.cisco.com ([173.37.255.144]) by rcdn-iport-6.cisco.com with ESMTP/TLS/TLS_AES_256_GCM_SHA384; 12 Jun 2026 12:15:22 +0000 Received: from sjc-ads-20495.cisco.com (sjc-ads-20495.cisco.com [171.70.188.248]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits) server-digest SHA256 client-signature RSA-PSS (4096 bits) client-digest SHA256) (Client CN "ciscoit-managed-infra-smtp-auth.cisco.com", Issuer "Internal Private TLS SubCA" (verified OK)) by rcdn-l-core-07.cisco.com (Postfix) with ESMTPS id C7A5C1800023E; Fri, 12 Jun 2026 12:15:21 +0000 (GMT) Received: by sjc-ads-20495.cisco.com (Postfix, from userid 1877012) id 4B45FCC1611; Fri, 12 Jun 2026 05:15:21 -0700 (PDT) From: "Ashishkumar Parmar X (asparmar - E INFOCHIPS PRIVATE LIMITED at Cisco)" To: openembedded-core@lists.openembedded.org Cc: xe-linux-external@cisco.com, Ashishkumar Parmar Subject: [OE-core][scarthgap][PATCH 1/6] rsync: Fix CVE-2026-29518 Date: Fri, 12 Jun 2026 05:13:29 -0700 Message-ID: <20260612121514.2282121-1-asparmar@cisco.com> X-Mailer: git-send-email 2.44.1 MIME-Version: 1.0 X-Auto-Response-Suppress: DR, OOF, AutoReply X-Outbound-Client-TLS: VERIFIED;sjc-ads-20495.cisco.com [171.70.188.248];TLSv1.3;TLS_AES_256_GCM_SHA384;256;ciscoit-managed-infra-smtp-auth.cisco.com X-Outbound-SMTP-Client: 171.70.188.248, sjc-ads-20495.cisco.com X-Outbound-Node: rcdn-l-core-07.cisco.com List-Id: X-Webhook-Received: from 45-33-107-173.ip.linodeusercontent.com [45.33.107.173] by aws-us-west-2-korg-lkml-1.web.codeaurora.org with HTTPS for ; Fri, 12 Jun 2026 12:15:30 -0000 X-Groupsio-URL: https://lists.openembedded.org/g/openembedded-core/message/238607 From: Ashishkumar Parmar 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 --- .../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 +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) + +CVE: CVE-2026-29518 +Upstream-Status: Backport [https://github.com/RsyncProject/rsync/commit/1a5ad81add1004354a3d8ba841b94ffe19cd2505] + +(cherry picked from commit 1a5ad81add1004354a3d8ba841b94ffe19cd2505) +Signed-off-by: Ashishkumar Parmar +--- + 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 +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 + +CVE: CVE-2026-29518 +Upstream-Status: Backport [https://github.com/RsyncProject/rsync/commit/99b36291d06ca66229942c7a525a1f5566f10c85] + +(cherry picked from commit 99b36291d06ca66229942c7a525a1f5566f10c85) +Signed-off-by: Ashishkumar Parmar +--- + 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" From patchwork Fri Jun 12 12:13:30 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Patchwork-Submitter: "Ashishkumar Parmar X (asparmar - E INFOCHIPS PRIVATE LIMITED at Cisco)" X-Patchwork-Id: 89915 Return-Path: X-Spam-Checker-Version: SpamAssassin 3.4.0 (2014-02-07) on aws-us-west-2-korg-lkml-1.web.codeaurora.org Received: from aws-us-west-2-korg-lkml-1.web.codeaurora.org (localhost.localdomain [127.0.0.1]) by smtp.lore.kernel.org (Postfix) with ESMTP id 37D0ECD98CE for ; Fri, 12 Jun 2026 12:15:30 +0000 (UTC) Received: from rcdn-iport-3.cisco.com (rcdn-iport-3.cisco.com [173.37.86.74]) by mx.groups.io with SMTP id smtpd.msgproc01-g2.69073.1781266525082939688 for ; Fri, 12 Jun 2026 05:15:25 -0700 Authentication-Results: mx.groups.io; dkim=fail reason="dkim: message contains an insecure body length tag" header.i=@cisco.com header.s=iport01 header.b=SKolFxee; spf=pass (domain: cisco.com, ip: 173.37.86.74, mailfrom: asparmar@cisco.com) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=cisco.com; i=@cisco.com; l=133376; q=dns/txt; s=iport01; t=1781266525; x=1782476125; h=from:to:cc:subject:date:message-id:in-reply-to: references:mime-version:content-transfer-encoding; bh=pRNQ/DfPY7hCCKJG9BPvUwSumYsZpdMf6iiyEQy3eeI=; b=SKolFxeeNudDgGxcPerGjwdJFa41uAeHgMauGmF/4HGeXzgr3lhjjV4F 3pWbizE5dOpzZhRv97fs2WQ8RSFI2rSL9S2w1lwSokex+Son0NbM+BRNU 1qAWo+gN5xNhO7GxK7NNUH83uNWcUTAFfwbX5lY68snqJa16gV9yS6c2T VE2KFSH4G4xDFiZLLBFcj8S8Jpzch/PuzG4y9yZ0nhluQTNKDB/ha0ygF YSsK7PpUDNfU69s3nb7Xrjs/YQTbUGTCcyLi7rSavRddL51KEziTorp2F 9O/Dyb0vzKF8f/qvwae+tUdjKyf0p7IYdPbbWbyaneYP6el9zwykKCJNG g==; X-CSE-ConnectionGUID: i4wuTzm1T+KMMorm2hugVA== X-CSE-MsgGUID: BVimmvsHRfSX0hxhOwcUng== X-IPAS-Result: A0AIAADf9ytq/4r/Ja1RCRoBAQEBAQEBAQEBAwEBAQESAQEBAQICAQEBAYF+AwEBAQELAYJWdF8ZKUkDhFSRdAOBE5A3hjaBPIIAgl8UgWoPAQEBD0QJBAQBAYISgnQCjUACJjYHDgECBAMCAwEBAQEBAQEBAQEBCwEBBQEBAQIBBwWBDhOGFQgyDYZaAQIBAxoBCAQLARgBLRAcAwECAwImAgIrIwgQCR+CYwGCcwIBEbJGGjd6fzOBAYMoAT8CQ1DbLAELFAEFgQUuAYU+gxwBhQJbGAGCSYIzJxsbgXKBFAGBFySBOHaBBYFcAgIBF4EACRESAQU6gzWCagSCInoSgVoDHjQeTIE3gX2KGUiBAhwDWSwBVRMNCgsHBYFmAzUSKhVuMh2BIz4XNFgbBwWBSoEraoEDhQ0jHwM5f4F0gShnaRUwNYEBARESAwsYDUgRLDcUGwQ9AW4HjCkZFw+BbwwjDgsCBAEBDR8EDBscBwkBCQEHAwkYGgYCJAIILwYMBwEWBR0DAQIhBQYBBB8BBBECCQYCDggPAgOSPQYdBwEBARMlj2WCIYE1njmBIQoog3WMIY9ChXgaM4QEgVeSQJE6gRcLmH2CWYsxlTQHLWgrhD2BbwcugVlwFTuCZwlKGQ9XijWCfx8DCwuDYIJWgj2BPcFBJDUCCQMvAQEHAgcOAwuBaJAAAiYEA25gAQE IronPort-Data: A9a23:t/AV2a34mQHivf4WtvbD5YJwkn2cJEfYwER7XKvMYLTBsI5bpzZTx jMbXTiBOquOajf2fIh3aoqwphwHusPRz98yGgU53Hw8FHgiRegpqji6wuYcGwvIc6UvmWo+t 512huHodZ5yFjmH4E/xbtANlFEkvYmQXL3wFeXYDS54QA5gWU8JhAlq8wIDqtYAbeORXUXX5 bsen+WFYAX7g2AtaDpNg06+gEoHUMra6WtwUmMWPZinjHeG/1EJAZQWI72GLneQauF8Au6gS u/f+6qy92Xf8g1FIovNfmHTKxBirhb6ZGBiu1IOM0SQqkEqSh8ajs7XAMEhhXJ/0F1lqTzeJ OJl7vRcQS9xVkHFdX90vxNwS0mSNoUekFPLzOTWXcG7lyX7n3XQL/pGDR0pILEK/79LQkJr0 PsCLWoNcx+9rrfjqF67YrEEasULNsLnOsYb/3pn1zycVa9gSpHYSKKM7thdtNsyrpkRRrCFO IxDNGcpNU+QC/FMEg9/5JYWlfywj2P6eidwo1OOrq1x6G/WpOB0+OS8bICLIoXQGa25mG6ym GPD+nriIisUD+zHySqF1W2nu+nAyHaTtIU6UefQGuRRqFqLy2oeDRcbWVe2rbyyjVSzc9ZeM FAPvC02oK4/8UamQtXwU1u/unHsg/IHc8BbH+t/7ESGzbDZpl7AQGMFVTVGLtchsafaWAAX6 7NApPuxbRQHjVFfYSv1Gmu8xd9qBRUoEA== IronPort-HdrOrdr: A9a23:8dkRuaHUvqK1MLQapLqENseALOsnbusQ8zAXPo5KJiC9Ffbo8f xG/c5rsiMc5wxxZJhNo7290ey7MBHhHP1OkO0s1NWZPDUO0VHAROoJ0WKh+UyEJ8SUzIBgPM lbH5SWIeeAdGSS9fyKgzWQIpIH3MSN9ryuiKP1yndgShwvVoRbhj0Jcjpy1iZNNXN77V1TLu vm2vZ6 X-Talos-CUID: 9a23:YTFhE2u59cxlILZAnhiwHcHe6IskLmLA8GXVenP7KkNMT6eQE1+N951Nxp8= X-Talos-MUID: 9a23:xlevNg29J7COWbfboUs39ZIR8zUjyZqvDUUTirU8tfK0bSxgOxCChwmxTdpy X-IronPort-Anti-Spam-Filtered: true X-IronPort-AV: E=Sophos;i="6.24,200,1774310400"; d="scan'208";a="494260124" Received: from rcdn-l-core-01.cisco.com ([173.37.255.138]) by rcdn-iport-3.cisco.com with ESMTP/TLS/TLS_AES_256_GCM_SHA384; 12 Jun 2026 12:15:22 +0000 Received: from sjc-ads-20495.cisco.com (sjc-ads-20495.cisco.com [171.70.188.248]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits) server-digest SHA256 client-signature RSA-PSS (4096 bits) client-digest SHA256) (Client CN "ciscoit-managed-infra-smtp-auth.cisco.com", Issuer "Internal Private TLS SubCA" (verified OK)) by rcdn-l-core-01.cisco.com (Postfix) with ESMTPS id DCD48180001C5; Fri, 12 Jun 2026 12:15:21 +0000 (GMT) Received: by sjc-ads-20495.cisco.com (Postfix, from userid 1877012) id 5B633CBF202; Fri, 12 Jun 2026 05:15:21 -0700 (PDT) From: "Ashishkumar Parmar X (asparmar - E INFOCHIPS PRIVATE LIMITED at Cisco)" To: openembedded-core@lists.openembedded.org Cc: xe-linux-external@cisco.com, Ashishkumar Parmar Subject: [OE-core][scarthgap][PATCH 2/6] rsync: Fix CVE-2026-43619 Date: Fri, 12 Jun 2026 05:13:30 -0700 Message-ID: <20260612121514.2282121-2-asparmar@cisco.com> X-Mailer: git-send-email 2.44.1 In-Reply-To: <20260612121514.2282121-1-asparmar@cisco.com> References: <20260612121514.2282121-1-asparmar@cisco.com> MIME-Version: 1.0 X-Auto-Response-Suppress: DR, OOF, AutoReply X-Outbound-Client-TLS: VERIFIED;sjc-ads-20495.cisco.com [171.70.188.248];TLSv1.3;TLS_AES_256_GCM_SHA384;256;ciscoit-managed-infra-smtp-auth.cisco.com X-Outbound-SMTP-Client: 171.70.188.248, sjc-ads-20495.cisco.com X-Outbound-Node: rcdn-l-core-01.cisco.com List-Id: X-Webhook-Received: from 45-33-107-173.ip.linodeusercontent.com [45.33.107.173] by aws-us-west-2-korg-lkml-1.web.codeaurora.org with HTTPS for ; Fri, 12 Jun 2026 12:15:30 -0000 X-Groupsio-URL: https://lists.openembedded.org/g/openembedded-core/message/238610 From: Ashishkumar Parmar 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 --- .../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 +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) + +CVE: CVE-2026-43619 +Upstream-Status: Backport [https://github.com/RsyncProject/rsync/commit/72d1cf1c288e5c526e906db2edafbf3d55762668] + +(cherry picked from commit 72d1cf1c288e5c526e906db2edafbf3d55762668) +Signed-off-by: Ashishkumar Parmar +--- + 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 + #endif + ++#ifdef __linux__ ++#include ++#include ++#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 +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) + +CVE: CVE-2026-43619 +Upstream-Status: Backport [https://github.com/RsyncProject/rsync/commit/61d987c54a472d88855c5fbef3a4c7b51696f93a] + +(cherry picked from commit 61d987c54a472d88855c5fbef3a4c7b51696f93a) +Signed-off-by: Ashishkumar Parmar +--- + 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 +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) + +CVE: CVE-2026-43619 +Upstream-Status: Backport [https://github.com/RsyncProject/rsync/commit/2b3f8aacc7eca828574a304a0f2b88fbcdaa04e3] + +(cherry picked from commit 2b3f8aacc7eca828574a304a0f2b88fbcdaa04e3) +Signed-off-by: Ashishkumar Parmar +--- + 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 +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) + +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 +--- + 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 ++ ++int dry_run = 0; ++int am_root = 0; ++int am_sender = 0; ++int read_only = 0; ++int list_only = 0; ++int copy_links = 0; ++int copy_unsafe_links = 0; ++extern int am_daemon, am_chrooted; ++ ++short info_levels[COUNT_INFO], debug_levels[COUNT_DEBUG]; ++ ++static int errs = 0; ++ ++static void check(const char *label, int actual_rc, int expect_ok, ++ const char *path, mode_t expected_mode) ++{ ++ struct stat st; ++ int got_ok = (actual_rc == 0); ++ if (got_ok != expect_ok) { ++ fprintf(stderr, "FAIL [%s]: rc=%d errno=%d (%s), expected %s\n", ++ label, actual_rc, errno, strerror(errno), ++ expect_ok ? "success" : "rejection"); ++ errs++; ++ return; ++ } ++ if (path && stat(path, &st) < 0) { ++ fprintf(stderr, "FAIL [%s]: stat(%s) failed: %s\n", ++ label, path, strerror(errno)); ++ errs++; ++ return; ++ } ++ if (path && (st.st_mode & 07777) != expected_mode) { ++ fprintf(stderr, ++ "FAIL [%s]: %s mode is 0%o, expected 0%o\n", ++ label, path, st.st_mode & 07777, expected_mode); ++ errs++; ++ return; ++ } ++ fprintf(stderr, "OK [%s]\n", label); ++} ++ ++int main(int argc, char **argv) ++{ ++ if (argc != 2) { ++ fprintf(stderr, "usage: %s \n", argv[0]); ++ return 2; ++ } ++ if (chdir(argv[1]) < 0) { ++ perror("chdir"); ++ return 2; ++ } ++ ++ /* Simulate the daemon-without-chroot deployment that do_chmod_at() ++ * defends. With am_daemon=0 or am_chrooted=1 the wrapper falls ++ * through to plain do_chmod() and the symlink-race test would be ++ * meaningless. */ ++ am_daemon = 1; ++ am_chrooted = 0; ++ ++ /* Test layout (all inside the directory we just chdir'd to): ++ * ++ * ./realdir/sentinel -- regular target file ++ * ./inside_link -> realdir -- legitimate dir-symlink within the tree ++ * ./escape_link -> ../trap -- attacker swap, target outside tree ++ * ../trap/sentinel -- the file the attacker wants to alter ++ * ++ * The shell wrapper that calls this helper has set both sentinel ++ * files to mode 0600 so we have a clean baseline to compare. ++ */ ++ ++ /* Scenario A: legitimate parent dir-symlink, chmod must succeed. */ ++ int rc = do_chmod_at("inside_link/sentinel", 0640); ++ check("A: legit dir-symlink within tree", ++ rc, 1, "realdir/sentinel", 0640); ++ ++ /* Scenario B: parent symlink escapes the tree -- chmod must be ++ * rejected and the outside file's mode must be unchanged. */ ++ rc = do_chmod_at("escape_link/sentinel", 0666); ++ check("B: parent symlink escapes tree (the attack)", ++ rc, 0, "../trap/sentinel", 0600); ++ ++ /* Scenario C: plain relative path with no symlink components, ++ * regression check that the safe wrapper doesn't break the ++ * normal case. */ ++ rc = do_chmod_at("realdir/sentinel", 0644); ++ check("C: plain relative path (regression check)", ++ rc, 1, "realdir/sentinel", 0644); ++ ++ /* Scenario D: top-level file, no parent directory component. ++ * Falls back to do_chmod(); should succeed. */ ++ rc = do_chmod_at("topfile", 0640); ++ check("D: top-level file, no parent component", ++ rc, 1, "topfile", 0640); ++ ++ if (errs) ++ fprintf(stderr, "%d failure(s)\n", errs); ++ return errs ? 1 : 0; ++} +diff --git a/t_stub.c b/t_stub.c +index 085378a8..904dac99 100644 +--- a/t_stub.c ++++ b/t_stub.c +@@ -23,6 +23,8 @@ + + int do_fsync = 0; + int inplace = 0; ++int am_daemon = 0; ++int am_chrooted = 0; + int modify_window = 0; + int preallocate_files = 0; + int protect_args = 0; +diff --git a/testsuite/chmod-symlink-race.test b/testsuite/chmod-symlink-race.test +new file mode 100755 +index 00000000..48bbfbb4 +--- /dev/null ++++ b/testsuite/chmod-symlink-race.test +@@ -0,0 +1,68 @@ ++#!/bin/sh ++ ++# Copyright (C) 2026 by Andrew Tridgell ++ ++# This program is distributable under the terms of the GNU GPL (see ++# COPYING). ++ ++# Regression test for the symlink-TOCTOU class of bug applied to ++# chmod() on the receiver side. The CVE-2026-29518 fix used ++# secure_relative_open() for the basis-file open, but every other ++# path-based syscall the receiver runs on sender-controllable paths ++# is vulnerable to the same primitive: a local attacker swaps a ++# symlink into one of the parent directory components between the ++# receiver's check and its act, and the syscall escapes the module. ++# ++# This test exercises the new do_chmod_at() wrapper via the ++# t_chmod_secure helper. The helper sets up two scenarios: ++# - a parent dir-symlink that resolves WITHIN the module tree ++# (legitimate -K-style use, must continue to work) ++# - a parent dir-symlink that escapes the module tree (the ++# attack, must be rejected) ++# plus two regression scenarios (plain relative path, top-level ++# file) that just confirm the safe wrapper doesn't break the ++# normal case. ++# ++# The kernel-enforced "stay below dirfd" path resolution is ++# only available on Linux 5.6+, FreeBSD 13+, and macOS 15+. ++# Skip on platforms that fall back to per-component O_NOFOLLOW ++# (Solaris, OpenBSD, NetBSD, Cygwin); the per-component fallback ++# would also reject the attack but the legitimate dir-symlink ++# scenario would fail there. ++ ++. "$suitedir/rsync.fns" ++ ++case "$(uname -s)" in ++ SunOS|OpenBSD|NetBSD|CYGWIN*) ++ test_skipped "do_chmod_at relies on RESOLVE_BENEATH-equivalent kernel support not available on $(uname -s)" ++ ;; ++esac ++ ++mod="$scratchdir/module" ++trap_outside="$scratchdir/trap" ++rm -rf "$mod" "$trap_outside" ++mkdir -p "$mod/realdir" "$trap_outside" ++ ++# Set up the four file-system objects the helper expects: ++echo bystander > "$mod/realdir/sentinel" ++chmod 0600 "$mod/realdir/sentinel" ++echo target > "$trap_outside/sentinel" ++chmod 0600 "$trap_outside/sentinel" ++ln -s realdir "$mod/inside_link" ++ln -s ../trap "$mod/escape_link" ++echo top > "$mod/topfile" ++chmod 0600 "$mod/topfile" ++ ++"$TOOLDIR/t_chmod_secure" "$mod" || \ ++ test_fail "t_chmod_secure reported failures (see stderr above)" ++ ++# Sanity-check from the shell side too: the outside file's mode must ++# still be 0600 -- the helper checked this, but a second look from ++# the shell guards against a helper-internal stat() bug. ++mode=$(stat -c '%a' "$trap_outside/sentinel" 2>/dev/null \ ++ || stat -f '%Lp' "$trap_outside/sentinel" 2>/dev/null) ++if [ "$mode" != "600" ]; then ++ test_fail "outside sentinel mode changed from 600 to $mode -- chmod escaped the module" ++fi ++ ++exit 0 +diff --git a/xattrs.c b/xattrs.c +index 65166eed..e5d0dd43 100644 +--- a/xattrs.c ++++ b/xattrs.c +@@ -1086,7 +1086,7 @@ int set_xattr(const char *fname, const struct file_struct *file, const char *fna + && !S_ISLNK(sxp->st.st_mode) + #endif + && access(fname, W_OK) < 0 +- && do_chmod(fname, (sxp->st.st_mode & CHMOD_BITS) | S_IWUSR) == 0) ++ && do_chmod_at(fname, (sxp->st.st_mode & CHMOD_BITS) | S_IWUSR) == 0) + added_write_perm = 1; + + ndx = F_XATTR(file); +@@ -1094,7 +1094,7 @@ int set_xattr(const char *fname, const struct file_struct *file, const char *fna + lst = &glst->xa_items; + int return_value = rsync_xal_set(fname, lst, fnamecmp, sxp); + if (added_write_perm) /* remove the temporary write permission */ +- do_chmod(fname, sxp->st.st_mode); ++ do_chmod_at(fname, sxp->st.st_mode); + return return_value; + } + +@@ -1211,7 +1211,7 @@ int set_stat_xattr(const char *fname, struct file_struct *file, mode_t new_mode) + mode = (fst.st_mode & _S_IFMT) | (fmode & ACCESSPERMS) + | (S_ISDIR(fst.st_mode) ? 0700 : 0600); + if (fst.st_mode != mode) +- do_chmod(fname, mode); ++ do_chmod_at(fname, mode); + if (!IS_DEVICE(fst.st_mode)) + fst.st_rdev = 0; /* just in case */ + +-- +2.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 +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) + +CVE: CVE-2026-43619 +Upstream-Status: Backport [https://github.com/RsyncProject/rsync/commit/d22b6bc7d1b1d7be9df1c0c6db1599cb7d5fd82c] + +(cherry picked from commit d22b6bc7d1b1d7be9df1c0c6db1599cb7d5fd82c) +Signed-off-by: Ashishkumar Parmar +--- + testsuite/sender-flist-symlink-leak.test | 90 ++++++++++++++++++++++++ + util1.c | 56 +++++++++++++-- + 2 files changed, 142 insertions(+), 4 deletions(-) + create mode 100755 testsuite/sender-flist-symlink-leak.test + +diff --git a/testsuite/sender-flist-symlink-leak.test b/testsuite/sender-flist-symlink-leak.test +new file mode 100755 +index 00000000..011d93d0 +--- /dev/null ++++ b/testsuite/sender-flist-symlink-leak.test +@@ -0,0 +1,90 @@ ++#!/bin/sh ++ ++# Copyright (C) 2026 by Andrew Tridgell ++ ++# This program is distributable under the terms of the GNU GPL (see ++# COPYING). ++ ++# Regression test for codex re-check finding: the sender-side file- ++# list generator can still follow an attacker-planted symlink out of ++# the module via change_pathname() -> change_dir(...,CD_SKIP_CHDIR) ++# followed by change_dir(...,CD_NORMAL). The CD_SKIP_CHDIR sets ++# skipped_chdir=1, and the next CD_NORMAL call's secure-branch in ++# util1.c is gated on `!skipped_chdir`, so the secure path is ++# bypassed and a raw chdir(curr_dir) follows attacker-controlled ++# symlinks during flist generation. ++# ++# Reach: rsync daemon module with `use chroot = no`. A local ++# attacker plants module/cd -> /outside. A client (innocent or ++# malicious) pulls rsync:////cd/. The daemon, as ++# sender, enumerates files in /outside and ships their metadata ++# (names, sizes, modes, mtimes) to the client. The actual content ++# transfer fails later at the secure_relative_open step with EXDEV, ++# but by then the metadata has already leaked. ++# ++# We detect by running a dry-run pull of the symlinked subdir and ++# checking whether the client's --list-only output mentions any ++# file from /outside. With the bug, /outside/secret.txt appears in ++# the list with its size; with the fix, the daemon's chdir into ++# the symlinked subdir is rejected and no /outside file is listed. ++ ++. "$suitedir/rsync.fns" ++ ++case "$(uname -s)" in ++ SunOS|OpenBSD|NetBSD|CYGWIN*) ++ test_skipped "secure change_dir relies on RESOLVE_BENEATH-equivalent kernel support not available on $(uname -s)" ++ ;; ++esac ++ ++mod="$scratchdir/module" ++outside="$scratchdir/outside" ++listfile="$scratchdir/listed.txt" ++conf="$scratchdir/test-rsyncd.conf" ++ ++rm -rf "$mod" "$outside" ++mkdir -p "$mod" "$outside" ++ ++# Outside-the-module file the daemon should NOT enumerate to clients. ++# A distinctive name + non-trivial size makes the leak easy to spot. ++echo "OUTSIDE_PROTECTED_FILE_USED_AS_LEAK_DETECTOR" > "$outside/leak_marker.txt" ++chmod 0644 "$outside/leak_marker.txt" ++ ++# The symlink trap planted by the local attacker. ++ln -s "$outside" "$mod/cd" ++ ++my_uid=`get_testuid` ++root_uid=`get_rootuid` ++root_gid=`get_rootgid` ++uid_setting="uid = $root_uid" ++gid_setting="gid = $root_gid" ++if test x"$my_uid" != x"$root_uid"; then ++ uid_setting="#$uid_setting" ++ gid_setting="#$gid_setting" ++fi ++ ++cat > "$conf" < "$listfile" 2>&1 || true ++ ++if grep -q "leak_marker\.txt" "$listfile"; then ++ echo "----- leaked listing follows" >&2 ++ sed 's/^/ /' "$listfile" >&2 ++ echo "----- leaked listing ends" >&2 ++ test_fail "sender flist leak: outside/leak_marker.txt was enumerated to the client (daemon's chdir followed the cd symlink during flist generation)" ++fi ++ ++exit 0 +diff --git a/util1.c b/util1.c +index 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 +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) + +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 +--- + 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 ++ ++int dry_run = 0; ++int am_root = 0; ++int am_sender = 0; ++int read_only = 0; ++int list_only = 0; ++int copy_links = 0; ++int copy_unsafe_links = 0; ++extern int am_daemon, am_chrooted; ++ ++short info_levels[COUNT_INFO], debug_levels[COUNT_DEBUG]; ++ ++static int errs = 0; ++ ++static void check_relpath(const char *relpath) ++{ ++ int fd; ++ int saved_errno; ++ ++ errno = 0; ++ fd = secure_relative_open(NULL, relpath, O_RDONLY | O_DIRECTORY, 0); ++ saved_errno = errno; ++ ++ if (fd >= 0) { ++ fprintf(stderr, ++ "FAIL [relpath=%-12s]: returned valid fd %d (escape) -- expected -1 EINVAL\n", ++ relpath, fd); ++ close(fd); ++ errs++; ++ return; ++ } ++ ++ if (saved_errno != EINVAL) { ++ fprintf(stderr, ++ "FAIL [relpath=%-12s]: rejected but errno=%d (%s), expected EINVAL\n", ++ relpath, saved_errno, strerror(saved_errno)); ++ errs++; ++ return; ++ } ++ ++ fprintf(stderr, "OK [relpath=%-12s]: rejected with EINVAL\n", relpath); ++} ++ ++static void check_basedir(const char *basedir) ++{ ++ int fd; ++ int saved_errno; ++ ++ errno = 0; ++ fd = secure_relative_open(basedir, "ok", O_RDONLY | O_DIRECTORY, 0); ++ saved_errno = errno; ++ ++ if (fd >= 0) { ++ fprintf(stderr, ++ "FAIL [basedir=%-12s]: returned valid fd %d -- expected -1 EINVAL\n", ++ basedir, fd); ++ close(fd); ++ errs++; ++ return; ++ } ++ ++ if (saved_errno != EINVAL) { ++ fprintf(stderr, ++ "FAIL [basedir=%-12s]: rejected but errno=%d (%s), expected EINVAL\n", ++ basedir, saved_errno, strerror(saved_errno)); ++ errs++; ++ return; ++ } ++ ++ fprintf(stderr, "OK [basedir=%-12s]: rejected with EINVAL\n", basedir); ++} ++ ++int main(int argc, char **argv) ++{ ++ if (argc != 2) { ++ fprintf(stderr, "usage: %s \n", argv[0]); ++ return 2; ++ } ++ if (chdir(argv[1]) < 0) { ++ perror("chdir"); ++ return 2; ++ } ++ ++ /* secure_relative_open's daemon-only confinement protections only ++ * fire when am_daemon && !am_chrooted (the threat model is the ++ * daemon-no-chroot deployment), but the front-door input ++ * validation runs unconditionally. We set am_daemon anyway so the ++ * helper exercises the same code shape the receiver does. */ ++ am_daemon = 1; ++ am_chrooted = 0; ++ ++ mkdir("subdir", 0755); ++ ++ /* Each of these relpaths must be rejected with EINVAL at the ++ * secure_relative_open() front door. ".." is the actual one-level ++ * escape; the others ("subdir/..", "subdir/../subdir") resolve ++ * back to the start dir on systems that allow them, but we still ++ * reject them as defence-in-depth: a path containing a ".." token ++ * is suspicious and the caller should normalise before passing ++ * it in. The "../foo" / "foo/../bar" / "/foo" / "/" cases are ++ * regression checks for the existing checks. */ ++ check_relpath(".."); ++ check_relpath("../foo"); ++ check_relpath("subdir/.."); ++ check_relpath("subdir/../subdir"); ++ check_relpath("foo/../bar"); ++ check_relpath("/foo"); ++ check_relpath("/"); ++ ++ /* Same checks against basedir (which the codex Finding 2 fix ++ * routes through the same RESOLVE_BENEATH-equivalent). Absolute ++ * basedirs are operator-trusted and intentionally not validated ++ * here. */ ++ check_basedir(".."); ++ check_basedir("../subdir"); ++ check_basedir("subdir/.."); ++ check_basedir("foo/../bar"); ++ ++ if (errs) ++ fprintf(stderr, "\n%d failure(s)\n", errs); ++ return errs ? 1 : 0; ++} +diff --git a/testsuite/alt-dest-symlink-race.test b/testsuite/alt-dest-symlink-race.test +new file mode 100755 +index 00000000..2256f2f2 +--- /dev/null ++++ b/testsuite/alt-dest-symlink-race.test +@@ -0,0 +1,96 @@ ++#!/bin/sh ++ ++# Copyright (C) 2026 by Andrew Tridgell ++ ++# This program is distributable under the terms of the GNU GPL (see ++# COPYING). ++ ++# Regression test for the basedir-confinement gap in ++# secure_relative_open(). The function opens basedir with a plain ++# openat(AT_FDCWD, basedir, O_RDONLY | O_DIRECTORY), without ++# RESOLVE_BENEATH or a per-component O_NOFOLLOW walk, so a parent ++# symlink ON basedir is followed unrestrictedly. RESOLVE_BENEATH is ++# then applied only to relpath, anchored at the wrong directory. ++# ++# The receiver's basis-file lookup at receiver.c passes ++# basis_dir[fnamecmp_type] (from --copy-dest / --link-dest / ++# --compare-dest -- all sender-controllable in daemon mode) as ++# basedir. A daemon-module attacker with write access can plant a ++# symlink at module/cd -> /outside, then run --link-dest=cd to ++# make the daemon's basis-file lookup resolve into /outside, ++# leaking the contents of daemon-readable files via the rsync ++# delta-rolling read-disclosure primitive. ++# ++# We detect the escape by leveraging --link-dest: when basis ++# matches source exactly (content + mtime + mode), --link-dest ++# hard-links the destination to the basis file. With the bug, the ++# destination ends up as a hard link to the outside-the-module ++# file (same inode). With the fix, no basis is found and the ++# destination is a fresh copy (different inode). ++# ++# The vulnerable code path is the same on every platform ++# (including the per-component fallback on systems without ++# RESOLVE_BENEATH), so this test is not platform-gated. ++ ++. "$suitedir/rsync.fns" ++ ++mod="$scratchdir/module" ++outside="$scratchdir/outside" ++src="$scratchdir/src" ++conf="$scratchdir/test-rsyncd.conf" ++ ++rm -rf "$mod" "$outside" "$src" ++mkdir -p "$mod" "$outside" "$src" ++ ++# Portable inode-number helper (GNU coreutils stat -c, BSD stat -f). ++file_inode() { ++ stat -c %i "$1" 2>/dev/null || stat -f %i "$1" ++} ++ ++# Outside-the-module file an attacker would like the daemon to ++# treat as a basis. ++echo "OUTSIDE_SECRET_DATA" > "$outside/target.txt" ++chmod 0644 "$outside/target.txt" ++ ++# The symlink trap planted in the module by the local attacker. ++ln -s "$outside" "$mod/cd" ++ ++# Source file matches outside/target.txt exactly (content + mtime ++# + mode) so --link-dest will hard-link the destination to the ++# basis file iff the daemon's basedir lookup reaches outside/. ++echo "OUTSIDE_SECRET_DATA" > "$src/target.txt" ++touch -r "$outside/target.txt" "$src/target.txt" ++chmod 0644 "$src/target.txt" ++ ++cat > "$conf" </dev/null 2>&1 || true ++ ++if [ ! -f "$mod/target.txt" ]; then ++ test_fail "destination file was not created -- daemon transfer failed before the test could observe the basedir behaviour" ++fi ++ ++outside_inode=$(file_inode "$outside/target.txt") ++dst_inode=$(file_inode "$mod/target.txt") ++ ++if [ "$outside_inode" = "$dst_inode" ]; then ++ test_fail "basedir-escape: --link-dest hard-linked module/target.txt to outside/target.txt (inode $outside_inode); daemon's basis-file lookup followed the parent symlink on the basedir" ++fi ++ ++exit 0 +diff --git a/testsuite/secure-relpath-validation.test b/testsuite/secure-relpath-validation.test +new file mode 100755 +index 00000000..5b77f7cc +--- /dev/null ++++ b/testsuite/secure-relpath-validation.test +@@ -0,0 +1,34 @@ ++#!/bin/sh ++ ++# Copyright (C) 2026 by Andrew Tridgell ++ ++# This program is distributable under the terms of the GNU GPL (see ++# COPYING). ++ ++# Regression test for codex audit Finding 5: secure_relative_open()'s ++# front-door input check rejects "../foo" and "foo/../bar" but ++# misses bare "..", "subdir/..", and other variants whose "/"-split ++# components contain a literal "..". The kernel-enforced ++# RESOLVE_BENEATH (Linux 5.6+) and O_RESOLVE_BENEATH ++# (FreeBSD 13+, macOS 15+) reject these in-kernel; the per-component ++# walk fallback used on NetBSD, OpenBSD, Solaris, Cygwin and pre-5.6 ++# Linux does not -- so the validation must happen at the front door. ++# ++# This test invokes the t_secure_relpath helper, which calls ++# secure_relative_open() with each suspect input and verifies the ++# return value is -1 with errno == EINVAL. EINVAL is the marker ++# that the front-door rejected the input, not the kernel; pre-fix ++# the kernel returns -1 with EXDEV (or, on the per-component ++# fallback, may return a valid fd at all -- "escape"). ++ ++. "$suitedir/rsync.fns" ++ ++testdir="$scratchdir/relpath-test" ++rm -rf "$testdir" ++mkdir -p "$testdir" ++ ++if ! "$TOOLDIR/t_secure_relpath" "$testdir"; then ++ test_fail "t_secure_relpath rejected one or more inputs incorrectly (see stderr above for the specific case)" ++fi ++ ++exit 0 +diff --git a/util1.c b/util1.c +index 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 +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) + +CVE: CVE-2026-43619 +Upstream-Status: Backport [https://github.com/RsyncProject/rsync/commit/a277a06b1017b4cf6bb0fe33d5823869ed02dfd9] + +(cherry picked from commit a277a06b1017b4cf6bb0fe33d5823869ed02dfd9) +Signed-off-by: Ashishkumar Parmar +--- + 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" </dev/null 2>&1 || true ++ ++verify_outside_unchanged "3b inplace+backup-dir=cd" ++ ++ ++############################################################ ++# Scenario 3c-symlink: fake-super symlink push to a path with a ++# symlinked parent ++# ++# With "fake super = yes" set on the module, the receiver ++# represents symlinks as fake-super files (regular files with the ++# link target written to them). The path-based open() in ++# do_symlink's fake-super branch follows parent symlinks. We push ++# a single symlink to the destination path "cd/sym" so the ++# receiver's create-file call lands at "cd/sym" relative to the ++# module root, where cd is the symlink trap. ++############################################################ ++ ++setup ++ ++mkdir -p "$src/cd" ++ln -s /etc/passwd "$src/cd/sym" ++ ++cat > "$conf" </dev/null 2>&1 || true ++ ++verify_outside_unchanged_or_absent "3c-symlink fake-super symlink push" "sym" ++ ++ ++############################################################ ++# Scenario 3c-mknod: fake-super FIFO push to a path with a ++# symlinked parent ++# ++# Similar to 3c-symlink but for special files. mkfifo works ++# without root; we push a FIFO and verify the receiver doesn't ++# create a fake-super file at outside/fifo. ++############################################################ ++ ++setup ++ ++mkdir -p "$src/cd" ++mkfifo "$src/cd/fifo" 2>/dev/null ++if [ ! -p "$src/cd/fifo" ]; then ++ test_skipped "mkfifo unavailable; cannot exercise 3c-mknod" ++fi ++ ++cat > "$conf" </dev/null 2>&1 || true ++ ++verify_outside_unchanged_or_absent "3c-mknod fake-super FIFO push" "fifo" ++ ++exit 0 +diff --git a/testsuite/copy-dest-source-symlink.test b/testsuite/copy-dest-source-symlink.test +new file mode 100755 +index 00000000..2d20fab4 +--- /dev/null ++++ b/testsuite/copy-dest-source-symlink.test +@@ -0,0 +1,83 @@ ++#!/bin/sh ++ ++# Copyright (C) 2026 by Andrew Tridgell ++ ++# This program is distributable under the terms of the GNU GPL (see ++# COPYING). ++ ++# Regression test for codex audit Finding 3a: copy_file()'s source ++# open in copy_altdest_file() is via do_open_nofollow(), which only ++# refuses a final-component symlink. Parent components are still ++# resolved with normal symlink-following. A daemon module attacker ++# who plants a parent symlink at module/cd -> /outside, then runs ++# --copy-dest=cd against a source file matching the size+mtime of ++# /outside/target.txt, drives the receiver to: ++# ++# 1. Find a match-level >= 2 basis at "cd/target.txt" ++# 2. Call copy_altdest_file -> copy_file(src="cd/target.txt", ...) ++# 3. do_open_nofollow follows the "cd" parent symlink and reads ++# the contents of /outside/target.txt under the daemon's ++# authority ++# 4. Copy that content into the module destination ++# ++# Result: outside/target.txt content lands at module/target.txt, ++# accessible to the attacker on a subsequent pull. ++# ++# We detect by content: src/target.txt and outside/target.txt have ++# identical metadata (size + mtime + mode) but different content. ++# After the transfer, module/target.txt should match src (no ++# basedir escape) -- if it matches outside, the bug copied across ++# the symlink boundary. ++ ++. "$suitedir/rsync.fns" ++ ++mod="$scratchdir/module" ++outside="$scratchdir/outside" ++src="$scratchdir/src" ++conf="$scratchdir/test-rsyncd.conf" ++ ++rm -rf "$mod" "$outside" "$src" ++mkdir -p "$mod" "$outside" "$src" ++ ++# Outside-the-module file the daemon should not read on the ++# attacker's behalf. ++echo "OUTSIDE_LEAKED_DATA!" > "$outside/target.txt" ++chmod 0644 "$outside/target.txt" ++ ++# The symlink trap. ++ln -s "$outside" "$mod/cd" ++ ++# Source: same size, same mtime, same mode as outside -- so the ++# generator's link_stat + quick_check_ok finds a match-level >= 2 ++# basis and calls copy_altdest_file. ++echo "ATTACKER_KNOWN_DATA!" > "$src/target.txt" ++touch -r "$outside/target.txt" "$src/target.txt" ++chmod 0644 "$src/target.txt" ++ ++cat > "$conf" </dev/null 2>&1 || true ++ ++if [ ! -f "$mod/target.txt" ]; then ++ test_fail "destination file was not created -- daemon transfer failed before the test could observe the basedir behaviour" ++fi ++ ++if cmp -s "$mod/target.txt" "$outside/target.txt"; then ++ test_fail "basedir-escape via copy_file source: module/target.txt now contains the contents of outside/target.txt -- daemon read /outside via the cd symlink and copied it into the module" ++fi ++ ++if ! cmp -s "$mod/target.txt" "$src/target.txt"; then ++ test_fail "destination doesn't match source content (and isn't outside content either): unexpected state" ++fi ++ ++exit 0 +diff --git a/util1.c b/util1.c +index 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" From patchwork Fri Jun 12 12:13:31 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: "Ashishkumar Parmar X (asparmar - E INFOCHIPS PRIVATE LIMITED at Cisco)" X-Patchwork-Id: 89912 Return-Path: X-Spam-Checker-Version: SpamAssassin 3.4.0 (2014-02-07) on aws-us-west-2-korg-lkml-1.web.codeaurora.org Received: from aws-us-west-2-korg-lkml-1.web.codeaurora.org (localhost.localdomain [127.0.0.1]) by smtp.lore.kernel.org (Postfix) with ESMTP id 791DCCD98DA for ; Fri, 12 Jun 2026 12:15:30 +0000 (UTC) Received: from rcdn-iport-1.cisco.com (rcdn-iport-1.cisco.com [173.37.86.72]) by mx.groups.io with SMTP id smtpd.msgproc01-g2.69072.1781266523020001377 for ; Fri, 12 Jun 2026 05:15:23 -0700 Authentication-Results: mx.groups.io; dkim=fail reason="dkim: message contains an insecure body length tag" header.i=@cisco.com header.s=iport01 header.b=QtZXECON; spf=pass (domain: cisco.com, ip: 173.37.86.72, mailfrom: asparmar@cisco.com) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=cisco.com; i=@cisco.com; l=9012; q=dns/txt; s=iport01; t=1781266523; x=1782476123; h=from:to:cc:subject:date:message-id:in-reply-to: references:mime-version:content-transfer-encoding; bh=isfBTwXdyKwhFBbwk3JyV1TjmcTg+/uByErIIPjJRNg=; b=QtZXECONAOiB9XhSFQpRdNae3J6+68QlUYly6X/sHw1Wc1Z5oLQMDbsF 4rN3NF5CmsWINXRnZKs7k09mf7i0c7Bm0B+KBkwJF95WUo3K8921VdNpm rl7kTBztWCrUg7A3BdQdqsOKfq5Xbk4RHwE8YOMF9f9xRfyPfFeH6uuVE HeK1dId+KXn/BqFsOULpS6GviSQnd/2EL/Tvu4sH8rKI2Rlgle8vmTVqh a+ks4yPSa21wdZ+pYtDlQzfYI1TvRIS1KMUdoiQ+ke9uYzlSfn3rlFYOL duAhRn20/gd3eKUJkIUJ1Pb+U5qnkmzwAHQjnmrNqHn1bc3qPZ2MSCqP2 Q==; X-CSE-ConnectionGUID: QY604+rPQI2awobnbQAyuQ== X-CSE-MsgGUID: gBngwDW6T5GV7ZDWB/ZVpw== X-IPAS-Result: A0BHAgDn9itq/5D/Ja1aHgEBCxIMggULgld0X0JJlksDnhuBfg8BAQEPRA0EAQGFBgKNQAImNAkOAQIEAwIDAQEBAQEBAQEBAQELAQEFAQEBAgEHBYEOE4ZPDYZaAQIBAzIBGAEtEBwDAQIvKyMIGYMCAYJzAgERsj0aN4IsgQGDKAE/AkNQ2ywBCxQBBYEzhT+IH1sYAYR8JxsbgXKBFAGBO4IugQWBXAICAReBDV+GHwSCDRV6EoFdHoh9hW5IgR4DWSwBVRMNCgsHBYFmAzUSKhVuMh2BIz4XgQwbBwWBSoEraoEDhQ0jHwM5f4F0gShnaRUwNYEBARESAwsYDUgRLDcUGwQ+bgeMQhcPgh4ZB4EOASsXGFAGSBtTkygBBwySK6EPCiiDdYwhj0KFeBozqUaBJguYfY4KllCEaIFoPIFEDgdwFYMiCUoZD44qAwsLg2CFE8J8JDUCCTIBAQcCBw4DC4FohGGLHwImB4FOAQE IronPort-Data: A9a23:HuwICKASzS3BLRVW/3jiw5YqxClBgxIJ4kV8jS/XYbTApDsr1mQDy mNLDD2PaayOM2LzKd90bIXi8hgH75GHyYNnOVdlrnsFo1CmBibm6XV1Cm+qYkt+++WaFBoPA /02M4eGdIZvCCeA+n9BC5C5xVFkz6aEW7HgP+DNPyF1VGdMRTwo4f5Zs7ZRbrVA357jX2thh fuo+5eBYAH8gWYtWo4pw/vrRC1H7ayaVAww5jTSVdgT1HfCmn8cCo4oJK3ZBxPQXolOE+emc P3Ixbe/83mx109F5gSNy+uTnuUiG9Y+DCDW4pZkc/HKbitq+kTe5p0G2M80Mi+7vdkmc+dZk 72hvbToIesg0zaldO41C3G0GAkmVUFKFSOuzXWX6aSuI0P6n3TE+sQwVB11J7Uh27x+HmZE8 /YhASEQV0XW7w626OrTpuhEnM8vKozveYgYoHwllW6fBvc9SpeFSKLPjTNa9G5v3YYVQrCEO pdfMGYzBPjDS0Un1lM/AYkmlf2tj2PXeDxDo1XTrq0yi4TW5FAgi+O9aYqPIbRmQ+1atECXu U75oVikLS9FBvCg4323zGqz07qncSTTHdh6+KeD3vlyjVuew2YeBBEbWR6wpuO0okq/QM5Eb UsM9ywjqKI/+ECmQp/6RRLQnZKflgQXV9wVF6gx7xuAj/KLpQ2YHWMDCDVGbbTKqfMLeNDj7 XfR9/uBONClmOT9pa61nltMkQ6PBA== IronPort-HdrOrdr: A9a23:EoGRvazby5MFS4MWuvw3KrPw9L1zdoMgy1knxilNoNJuHfBw8P re+8jzuiWUtN98YhwdcJW7Scu9qBDnhPpICOsqXYtKNTOO0ADDEGgh1/qG/9SKIUPDH4BmuZ uIWpIObuEYdWIK7vrS0U2fD8sqxsWB/eSDgOfTyGoocCRRApsQljuQzm2gYzZLrM4sP+tAKK ah X-Talos-CUID: 9a23:h8cea2ywedI/e5tEWa7xBgUtK559Lj7H9E7xeUanDmxvQoS8WEKprfY= X-Talos-MUID: 9a23:Fa14iA9LPslwj48mJAyBaeuQf8hC5auxJH9TrcgphZa2GRNafAWjlCviFw== X-IronPort-Anti-Spam-Filtered: true X-IronPort-AV: E=Sophos;i="6.24,200,1774310400"; d="scan'208";a="493381823" Received: from rcdn-l-core-07.cisco.com ([173.37.255.144]) by rcdn-iport-1.cisco.com with ESMTP/TLS/TLS_AES_256_GCM_SHA384; 12 Jun 2026 12:15:22 +0000 Received: from sjc-ads-20495.cisco.com (sjc-ads-20495.cisco.com [171.70.188.248]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits) server-digest SHA256 client-signature RSA-PSS (4096 bits) client-digest SHA256) (Client CN "ciscoit-managed-infra-smtp-auth.cisco.com", Issuer "Internal Private TLS SubCA" (verified OK)) by rcdn-l-core-07.cisco.com (Postfix) with ESMTPS id E1485180004B3; Fri, 12 Jun 2026 12:15:21 +0000 (GMT) Received: by sjc-ads-20495.cisco.com (Postfix, from userid 1877012) id 7D646CBF203; Fri, 12 Jun 2026 05:15:21 -0700 (PDT) From: "Ashishkumar Parmar X (asparmar - E INFOCHIPS PRIVATE LIMITED at Cisco)" To: openembedded-core@lists.openembedded.org Cc: xe-linux-external@cisco.com, Ashishkumar Parmar Subject: [OE-core][scarthgap][PATCH 3/6] rsync: Fix CVE-2026-43618 Date: Fri, 12 Jun 2026 05:13:31 -0700 Message-ID: <20260612121514.2282121-3-asparmar@cisco.com> X-Mailer: git-send-email 2.44.1 In-Reply-To: <20260612121514.2282121-1-asparmar@cisco.com> References: <20260612121514.2282121-1-asparmar@cisco.com> MIME-Version: 1.0 X-Auto-Response-Suppress: DR, OOF, AutoReply X-Outbound-Client-TLS: VERIFIED;sjc-ads-20495.cisco.com [171.70.188.248];TLSv1.3;TLS_AES_256_GCM_SHA384;256;ciscoit-managed-infra-smtp-auth.cisco.com X-Outbound-SMTP-Client: 171.70.188.248, sjc-ads-20495.cisco.com X-Outbound-Node: rcdn-l-core-07.cisco.com List-Id: X-Webhook-Received: from 45-33-107-173.ip.linodeusercontent.com [45.33.107.173] by aws-us-west-2-korg-lkml-1.web.codeaurora.org with HTTPS for ; Fri, 12 Jun 2026 12:15:30 -0000 X-Groupsio-URL: https://lists.openembedded.org/g/openembedded-core/message/238605 From: Ashishkumar Parmar 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 --- .../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 +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) + +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 +--- + 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" From patchwork Fri Jun 12 12:13:32 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: "Ashishkumar Parmar X (asparmar - E INFOCHIPS PRIVATE LIMITED at Cisco)" X-Patchwork-Id: 89911 Return-Path: X-Spam-Checker-Version: SpamAssassin 3.4.0 (2014-02-07) on aws-us-west-2-korg-lkml-1.web.codeaurora.org Received: from aws-us-west-2-korg-lkml-1.web.codeaurora.org (localhost.localdomain [127.0.0.1]) by smtp.lore.kernel.org (Postfix) with ESMTP id 904EDCD98DB for ; Fri, 12 Jun 2026 12:15:30 +0000 (UTC) Received: from rcdn-iport-1.cisco.com (rcdn-iport-1.cisco.com [173.37.86.72]) by mx.groups.io with SMTP id smtpd.msgproc01-g2.69072.1781266523020001377 for ; Fri, 12 Jun 2026 05:15:23 -0700 Authentication-Results: mx.groups.io; dkim=fail reason="dkim: message contains an insecure body length tag" header.i=@cisco.com header.s=iport01 header.b=KzM3qw1J; spf=pass (domain: cisco.com, ip: 173.37.86.72, mailfrom: asparmar@cisco.com) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=cisco.com; i=@cisco.com; l=6360; q=dns/txt; s=iport01; t=1781266523; x=1782476123; h=from:to:cc:subject:date:message-id:in-reply-to: references:mime-version:content-transfer-encoding; bh=9a852K88RSP8tXCCKuY8p/79wrM3tNNJSSJuBmVtNy4=; b=KzM3qw1Jbda6rAXGgPgloT8QrC9chK32PNJxZtO9IjWRO+B9ilk3SmK2 Y7jEL3zEnANDQJ/MjYuyKy7giobX9dWErg5zGo3Q7wIlwhKPt6PviwVZH q8tQECWbDJlIWmEamGE6QpaG53Nw/BhDN104d92nwfjUxiXfPV0RynFxm o4mx2qQnhJKQ8pGzX9ty6Z0UVAHwUyjcu+87h+znb0bled+I45qvWYo90 HBsiD1dHapkZifv11fU4gV8s+PI0wCMfKZjVKsmmAliQn9ftYD9whDiTg xHWvv1cxH4BLzAAW5mh3R6bRf37NOQWcfk9SFW+9AoZE7tTxe/gaRIYUx g==; X-CSE-ConnectionGUID: QfhxTo9NRJutymKpN9ZsmA== X-CSE-MsgGUID: /hiwH158Qeis/ZJx8o4bmg== X-IPAS-Result: A0BIAgDn9itq/5T/Ja1aglmCV3RfQkmWSwOeG4F+DwEBAQ9EDQQBAYUGAo1AAiY0CQ4BAgQDAgMBAQEBAQEBAQEBAQsBAQUBAQECAQcFgQ4Thk8NhloBAgEDJwsBGAEtEBwDAQIvKyMIEAmDAgGCcwIBEbI9GjeBeTOBAYMoAT8CQ1DbLAELFAEFgTOFP4gfWxgBgkmCMycbG4FygRWBO4IugQWBXAICAReBDSGGXQSCInoSgVoDHoJVjBZIgR4DWSwBVRMNCgsHBYFmAzUSKhVuMh2BIz4XgQwbBwWBSoEraoEDhQ0jHwM5f4F0gShnaRUwNYEBARESAwsYDUgRLDcUGwQ+bgeMQhcPgh4gAR5vASsXaBmnCqEPCiiDdYwhj0KFeBozhASUF5JRC5h9glmLMZU0gRyEaIFoPIFZcBU7gmcJShkPji0LC4NghRPCfCQ1AgkyAQEHAgcOAwuBaJAAAiYHgU4BAQ IronPort-Data: A9a23:9fuU3a2+IgSEySqqrvbD5YJwkn2cJEfYwER7XKvMYLTBsI5bp2AAm mYbWW2EO/uJNmejLtgnO9/lpE4B65eBxtVhGQNu3Hw8FHgiRegpqji6wuYcGwvIc6UvmWo+t 512huHodZ5yFjmH4E/xbtANlFEkvYmQXL3wFeXYDS54QA5gWU8JhAlq8wIDqtYAbeORXUXX5 bsen+WFYAX7g2AtaDpNg06+gEoHUMra6WtwUmMWPZinjHeG/1EJAZQWI72GLneQauF8Au6gS u/f+6qy92Xf8g1FIovNfmHTKxBirhb6ZGBiu1IOM0SQqkEqSh8ajs7XAMEhhXJ/0F1lqTzeJ OJl7vRcQS9xVkHFdX90vxNwS0mSNoUekFPLzOTWXcG7lyX7n3XQL/pGNU82LbEfq8BNI3xS5 Pk7MBZcXAuGrrfjqF67YrEEasULNsLnOsYb/3pn1zycVK9gSpHYSKKM7thdtNsyrpkRRrCFO IxDNGcpNUiQC/FMEg9/5JYWlfywj2P6eidwo1OOrq1x6G/WpOB0+OS8a4GFI4zQGa25mG7fp mPZ3z2kUigXNcfY+xiVonXwjOrAyHaTtIU6UefQGuRRqFqLy2oeDRcbWVe2rbyyjVSzc9ZeM FAPvC02oK4/8UamQtXwU1u/unHsg/IHc8BbH+t/7ESGzbDZpl/AQGMFVTVGLtchsafaWAAX6 7NApPuxbRQHjVFfYSn1Gmu8xd9qBRUoEA== IronPort-HdrOrdr: A9a23:0ksOH6DfWXqr8OTlHel055DYdb4zR+YMi2TDGXofdfUzSL3+qy nAppUmPHPP5Qr5HUtQ++xoW5PwJU80l6QU3WB5B97LN2PbUSmTXeRfBODZrQEIdReTygd179 YHT0EHMqySMXFKyeDn/QK/D9EshPOD8KyumKPi6k0Fd3ASV0mlhD0JcTpy1SZNNXF7OaY= X-Talos-CUID: 9a23:GRIQXG+UUuan/OQvx16Vv3caGtAZdSz79lXNP169Kn5PGY+OTHbFrQ== X-Talos-MUID: 9a23:+WWnXQZlHPk/nuBT8D/tmxE7aZdR7qW1T3IvgcUB58OBOnkl X-IronPort-Anti-Spam-Filtered: true X-IronPort-AV: E=Sophos;i="6.24,200,1774310400"; d="scan'208";a="493381824" Received: from rcdn-l-core-11.cisco.com ([173.37.255.148]) by rcdn-iport-1.cisco.com with ESMTP/TLS/TLS_AES_256_GCM_SHA384; 12 Jun 2026 12:15:22 +0000 Received: from sjc-ads-20495.cisco.com (sjc-ads-20495.cisco.com [171.70.188.248]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits) server-digest SHA256 client-signature RSA-PSS (4096 bits) client-digest SHA256) (Client CN "ciscoit-managed-infra-smtp-auth.cisco.com", Issuer "Internal Private TLS SubCA" (verified OK)) by rcdn-l-core-11.cisco.com (Postfix) with ESMTPS id 26ED018000149; Fri, 12 Jun 2026 12:15:22 +0000 (GMT) Received: by sjc-ads-20495.cisco.com (Postfix, from userid 1877012) id A84C8CBF204; Fri, 12 Jun 2026 05:15:21 -0700 (PDT) From: "Ashishkumar Parmar X (asparmar - E INFOCHIPS PRIVATE LIMITED at Cisco)" To: openembedded-core@lists.openembedded.org Cc: xe-linux-external@cisco.com, Ashishkumar Parmar Subject: [OE-core][scarthgap][PATCH 4/6] rsync: Fix CVE-2026-43620 Date: Fri, 12 Jun 2026 05:13:32 -0700 Message-ID: <20260612121514.2282121-4-asparmar@cisco.com> X-Mailer: git-send-email 2.44.1 In-Reply-To: <20260612121514.2282121-1-asparmar@cisco.com> References: <20260612121514.2282121-1-asparmar@cisco.com> MIME-Version: 1.0 X-Auto-Response-Suppress: DR, OOF, AutoReply X-Outbound-Client-TLS: VERIFIED;sjc-ads-20495.cisco.com [171.70.188.248];TLSv1.3;TLS_AES_256_GCM_SHA384;256;ciscoit-managed-infra-smtp-auth.cisco.com X-Outbound-SMTP-Client: 171.70.188.248, sjc-ads-20495.cisco.com X-Outbound-Node: rcdn-l-core-11.cisco.com List-Id: X-Webhook-Received: from 45-33-107-173.ip.linodeusercontent.com [45.33.107.173] by aws-us-west-2-korg-lkml-1.web.codeaurora.org with HTTPS for ; Fri, 12 Jun 2026 12:15:30 -0000 X-Groupsio-URL: https://lists.openembedded.org/g/openembedded-core/message/238606 From: Ashishkumar Parmar 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 --- .../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 +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) + +CVE: CVE-2026-43620 +Upstream-Status: Backport [https://github.com/RsyncProject/rsync/commit/0a5fa00fdcbacbebb89daca0ae68ae320f22dc74] + +(cherry picked from commit 0a5fa00fdcbacbebb89daca0ae68ae320f22dc74) +Signed-off-by: Ashishkumar Parmar +--- + 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" From patchwork Fri Jun 12 12:13:33 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: "Ashishkumar Parmar X (asparmar - E INFOCHIPS PRIVATE LIMITED at Cisco)" X-Patchwork-Id: 89913 Return-Path: X-Spam-Checker-Version: SpamAssassin 3.4.0 (2014-02-07) on aws-us-west-2-korg-lkml-1.web.codeaurora.org Received: from aws-us-west-2-korg-lkml-1.web.codeaurora.org (localhost.localdomain [127.0.0.1]) by smtp.lore.kernel.org (Postfix) with ESMTP id 5D4AACD98D9 for ; Fri, 12 Jun 2026 12:15:30 +0000 (UTC) Received: from rcdn-iport-4.cisco.com (rcdn-iport-4.cisco.com [173.37.86.75]) by mx.groups.io with SMTP id smtpd.msgproc02-g2.69133.1781266523300532639 for ; Fri, 12 Jun 2026 05:15:23 -0700 Authentication-Results: mx.groups.io; dkim=fail reason="dkim: message contains an insecure body length tag" header.i=@cisco.com header.s=iport01 header.b=hgQZwuQC; spf=pass (domain: cisco.com, ip: 173.37.86.75, mailfrom: asparmar@cisco.com) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=cisco.com; i=@cisco.com; l=8947; q=dns/txt; s=iport01; t=1781266523; x=1782476123; h=from:to:cc:subject:date:message-id:in-reply-to: references:mime-version:content-transfer-encoding; bh=3hKTyk8nx28g6a7/aBLMCi5ucygOWeWEnGkK2fYu0Lk=; b=hgQZwuQCsijDYmutfZjukiF9CBxMnFQ6VpYusbzp3oPolLtb04E1eJvz d+KxaL7ImzqxWkOaKNW+FiMrNzyUwolSyJwYcwBz+5wERsCLDA92Y3WkV Z28S1ok8G9ylMJcOMyh3viNO+x9vs1vT2RSs7xptunNbz2whwFh2XxFvW d9ZR65BBHm8APsCubUYv08g4BnCfCsOz258096AX1TdkXSBKHR/fkfzaK nf83k2jXVhsnUp5KrDi0iRijKBjLQebtWaN718STmAZa3HFdag3LVDxaE pMMDDTSsVTx+vEHzXQJUPgmtlqOr0+i8WsVS9jeJJYX4x9wNcXvU14Tgt g==; X-CSE-ConnectionGUID: aCi8muYNT5+gYJcpolACbA== X-CSE-MsgGUID: 1R3kziwdRau6ln4ykEz+Hg== X-IPAS-Result: A0AnAADn9itq/5H/Ja1aHQEBAQEJARIBBQUBgXwIAQsBglZ0X0JJjHOJWAOeGxSBag8BAQEPRA0EAQGFBgKNQAImNAkOAQIEAwIDAQEBAQEBAQEBAQELAQEFAQEBAgEHBYEOE4ZPDYZaAQIBAzIBGAEtEBwDAQIvKyMIEAmDAgGCcwIBEbI9GjeCLIEBgygBPwJDUNssAQsUAQWBMwGFPogfWxgBRIQ4JxsbgXKBFYE7gi6BBYFcAgIBF4EJhwIEgiJ6EoFdHjSDIYsWSIEeA1ksAVUTDQoLBwWBZgM1EioVbjIdgSM+F4EMGwcFgUqBK2qBA4UNIx8DOX+BdIEoZ2kVMDWBAQEREgMLGA1IESw3FBsEPm4HjEIXD4FOaQcBehMBK0Y5NR0bHBcpHhGSVwwbJ49lgiGBNZ9aCiiDdYwhj0KFeBozhASUF5JRC5h9jgqVNIEchGiBaDyBQw8HcBWDIglKGQ+OKAULC4NghRPCfCQ1AgkyAQEHAgcOAwuBaJF9AQE IronPort-Data: A9a23:KIn5wqhm8U0DT5lZAjEzXOGrX161MREKZh0ujC45NGQN5FlHY01je htvXmGHOfnYMGX8c4glbN61oBwO75HUnIM3SQRr+S9hQSljpJueD7x1DKtf0wB+jyHnZBg6h ynLQoCYdKjYdleF+FH1dOOn9SUgvU2xbuKUIPbePSxsThNTRi4kiBZy88Y0mYcAbeKRW2thg vus5ZeDULOZ82QsaDxMtfvZ8EoHUMna4Vv0gHRvPZing3eG/5UlJMp3Db28KXL+Xr5VEoaSL 87fzKu093/u5BwkDNWoiN7TKiXmlZaLYGBiIlIPM0STqkAqSh4ai87XB9JAAatjsAhlqvgqo Dl7WTNcfi9yVkHEsLx1vxC1iEiSN4UekFPMCSDXXcB+UyQqflO0q8iCAn3aMqUR0ecnBHx/0 8BFaz88TBubgOaf67iSH7wEasQLdKEHPasFsX1miDWcBvE8TNWbE+PB5MRT23E7gcUm8fT2P pVCL2EwKk6dPlsWZgp/5JEWxI9EglH2aCVRslecv4I84nPYy0p6172F3N/9Jo3RGpUNwh/Cz o7A1z3DWRIINcOT8zOYrHaq2v7MsDG4eI1HQdVU8dYv2jV/3Fc7DwUbU1a+q/S1hkOyHtlYM UE8/is1sbN081SmSNT4VRC0rHOI+BkGVLJt//YS8gqBzO/Qpg2eHGVBFm4HY909v8hwTjsvv rOUo+7U6fVUmOX9YRqgGn2891te5QB9wbc+WBI5 IronPort-HdrOrdr: A9a23:OPrpqKsLjBcsYMbaHNncZ0sR7skDrtV00zEX/kB9WHVpmwKj+P xG+85rsiMc5wxxZJhNo7290ey7MBHhHP1OkO0s1NWZPDUO0VHAROoJ0WKh+UyEJ8SUzIBgPM lbH5SWIeeAa2SS9fyKgzWQIpIH3MSN9ryuiKP1yndgShwvVoRbhj0Jczpy1iZNNXJ77V1TLu vl2vZ6 X-Talos-CUID: 9a23:GB/HpGxmh6mWtf0AA5VCBgUdMcF1d2Ds6kuNYECJK21xT7m8YlW5rfY= X-Talos-MUID: 9a23:Mt5negjDr2s5/k57/j1q/cMpbf0z4LavF0w3mIhYv8/YJC9dKXS9g2Hi X-IronPort-Anti-Spam-Filtered: true X-IronPort-AV: E=Sophos;i="6.24,200,1774310400"; d="scan'208";a="493780412" Received: from rcdn-l-core-08.cisco.com ([173.37.255.145]) by rcdn-iport-4.cisco.com with ESMTP/TLS/TLS_AES_256_GCM_SHA384; 12 Jun 2026 12:15:22 +0000 Received: from sjc-ads-20495.cisco.com (sjc-ads-20495.cisco.com [171.70.188.248]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits) server-digest SHA256 client-signature RSA-PSS (4096 bits) client-digest SHA256) (Client CN "ciscoit-managed-infra-smtp-auth.cisco.com", Issuer "Internal Private TLS SubCA" (verified OK)) by rcdn-l-core-08.cisco.com (Postfix) with ESMTPS id 3E4FC180001EE; Fri, 12 Jun 2026 12:15:22 +0000 (GMT) Received: by sjc-ads-20495.cisco.com (Postfix, from userid 1877012) id D39C5CBEF8A; Fri, 12 Jun 2026 05:15:21 -0700 (PDT) From: "Ashishkumar Parmar X (asparmar - E INFOCHIPS PRIVATE LIMITED at Cisco)" To: openembedded-core@lists.openembedded.org Cc: xe-linux-external@cisco.com, Ashishkumar Parmar Subject: [OE-core][scarthgap][PATCH 5/6] rsync: Fix CVE-2026-43617 Date: Fri, 12 Jun 2026 05:13:33 -0700 Message-ID: <20260612121514.2282121-5-asparmar@cisco.com> X-Mailer: git-send-email 2.44.1 In-Reply-To: <20260612121514.2282121-1-asparmar@cisco.com> References: <20260612121514.2282121-1-asparmar@cisco.com> MIME-Version: 1.0 X-Auto-Response-Suppress: DR, OOF, AutoReply X-Outbound-Client-TLS: VERIFIED;sjc-ads-20495.cisco.com [171.70.188.248];TLSv1.3;TLS_AES_256_GCM_SHA384;256;ciscoit-managed-infra-smtp-auth.cisco.com X-Outbound-SMTP-Client: 171.70.188.248, sjc-ads-20495.cisco.com X-Outbound-Node: rcdn-l-core-08.cisco.com List-Id: X-Webhook-Received: from 45-33-107-173.ip.linodeusercontent.com [45.33.107.173] by aws-us-west-2-korg-lkml-1.web.codeaurora.org with HTTPS for ; Fri, 12 Jun 2026 12:15:30 -0000 X-Groupsio-URL: https://lists.openembedded.org/g/openembedded-core/message/238608 From: Ashishkumar Parmar 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 --- .../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 +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 = " 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) + +CVE: CVE-2026-43617 +Upstream-Status: Backport [https://github.com/RsyncProject/rsync/commit/74ea276900779b95ddd1769d1d6ae78b2fd1a790] + +(cherry picked from commit 74ea276900779b95ddd1769d1d6ae78b2fd1a790) +Signed-off-by: Ashishkumar Parmar +--- + 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" <"$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" From patchwork Fri Jun 12 12:13:34 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: "Ashishkumar Parmar X (asparmar - E INFOCHIPS PRIVATE LIMITED at Cisco)" X-Patchwork-Id: 89914 Return-Path: X-Spam-Checker-Version: SpamAssassin 3.4.0 (2014-02-07) on aws-us-west-2-korg-lkml-1.web.codeaurora.org Received: from aws-us-west-2-korg-lkml-1.web.codeaurora.org (localhost.localdomain [127.0.0.1]) by smtp.lore.kernel.org (Postfix) with ESMTP id 89FA7CD98DC for ; Fri, 12 Jun 2026 12:15:30 +0000 (UTC) Received: from rcdn-iport-4.cisco.com (rcdn-iport-4.cisco.com [173.37.86.75]) by mx.groups.io with SMTP id smtpd.msgproc02-g2.69133.1781266523300532639 for ; Fri, 12 Jun 2026 05:15:24 -0700 Authentication-Results: mx.groups.io; dkim=fail reason="dkim: message contains an insecure body length tag" header.i=@cisco.com header.s=iport01 header.b=J1nWYsLN; spf=pass (domain: cisco.com, ip: 173.37.86.75, mailfrom: asparmar@cisco.com) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=cisco.com; i=@cisco.com; l=10211; q=dns/txt; s=iport01; t=1781266524; x=1782476124; h=from:to:cc:subject:date:message-id:in-reply-to: references:mime-version:content-transfer-encoding; bh=JoYiaEjw0vBg0HSOFENHLEL1Z5IMDELFfpDqkfYgfmI=; b=J1nWYsLN7Pcje4XhzQBt9+IE+yGNXYLL2Wi8vNQgnl3Wi9Qthy9GHQWJ NTaVOvleV8aP+Yfqi/hzbQDGeLpzxfYRXLmTihwDnrt8qo79qY3hld5ct 5xvt7l85NZvj+V8P6Kf/a3aku0Dl54PgmVuVm0oOUAxMLztGf3v1QaWEq hRu+yXMbifUxvqtoL6tgkFSyF4O7wRkLZFhG+gytI5BaCsrcc51gjco7O NIyyP8lxZPA/LTPAKGwZJFKwvsfAhbRj6td4AKNUG7LuP3tE3LH0S6WiR rHL39MwY+w1oQAdor+3EM3UnDfKsGZmG/AiUxTwOHxq+7NZ7WUNd8NnoZ Q==; X-CSE-ConnectionGUID: eCwuLf3eSFClUF8rkG9oPQ== X-CSE-MsgGUID: XKvUJWfhSqeMMTOEYMflpA== X-IPAS-Result: A0BIAgDn9itq/5D/Ja1aHgEBCxIMggULgld0X0JJlksDgROQN4xRFIFqDwEBAQ9EDQQBAYUGAo1AAiY0CQ4BAgQDAgMBAQEBAQEBAQEBAQsBAQUBAQECAQcFgQ4Thk8NhloBAgEDMgEYAS0QHAMBAi8rIwgZgwIBgnMCARGyPRo3giyBAYMoAT8CQ1DbLAELFAEFgTOFP4gfWxgBhHwnGxuBcoEVgTuBOHaBBYFcAgIBF4EJhwIEgiJ6EoFdHoIFgk2KGUiBHgNZLAFVEw0KCwcFgWYDNRIqFW4yHYEjPheBDBsHBYFKgStqgQOFDSMfAzl/gXSBKGdpFTA1gQEBERIDCxgNSBEsNxQbBD5uB4wpGRcPggYxBwZNKBMBKgEXeg8UHYEpkmAJOZIGgTWfWgoog3WMIZU6GjOEBJQXklELmH2OCpU2LC1BhGiBaDyBWXAVgyIJShkPjioDCwuDYIUTwnwkNQIJMgEBBwIHDgMLgWiRfQEB IronPort-Data: A9a23:QHmUYq6nQCMtG6DaWrX6EQxRtGnGchMFZxGqfqrLsTDasY5as4F+v mcYUDuGOq2IazCmfdF/Ydvn/E5U75fdx9VmSQE+/H1nZn8b8sCt6fZ1gavT04J+CuWZESqLO u1HMoGowPgcFyGa/lH2dOC98RGQ7InQLpLkEunIJyttcgFtTSYlmHpLlvUw6mJSqYDR7zil5 5Wo/6UzBHf/g2QqajxNsfrawP9SlK2aVA0w7wRWic9j5Dcyp1FNZLoDKKe4KWfPQ4U8NoaSW +bZwbilyXjS9hErB8nNuu6TnpoiG+O60aCm0xK6aoD66vRwjnVaPpUTaJLwXXxqZwChxLid/ jniWauYEm/FNoWU8AgUvoIx/ytWZcWq85efSZSzXFD6I0DuKxPRL/tS4E4eMKYH58J5Wmx12 9ccFR5ddBOs1uWO+efuIgVsrpxLwMjDJogTvDRkiDreF/tjGc+FSKTR7tge1zA17ixMNa+BP IxCNnw1MUmGOkETUrsUIMpWcOOAj2LneiddoUi9rqss6G+Vxwt0uFToGIaEIYPaHZkMxS50o Er23HrEIC4UNOeGxB+gzlWBt8rBmTvSDdd6+LqQs6QCbEeo7msLBRsbUFG2rfW0hgu1XMhSA 0gV4TY1668q+UqmS9PwUxG1rDiDpBF0ZjZLO/cx5AfIzu/f5ByUQzBbCDVAc9ch8sQxQFTGy 2O0oj8gPhQ32JX9dJ5X3uz8Qe+aUcTNEVI/WA== IronPort-HdrOrdr: A9a23:/6yNZaB3J43tayrlHel055DYdb4zR+YMi2TDGXofdfUzSL3+qy nAppUmPHPP5Qr5HUtQ++xoW5PwJU80l6QU3WB5B97LN2PbUSmTXeRfBODZrQEIdReTygd179 YHT0EHMqySMXFKyeDn/QK/D9EshPOD8KyumKPi6k0Fd3ASV0mlhD0JcTpy1SZNNXF7OaY= X-Talos-CUID: 9a23:/fwGC27zYp++zyRkctss+WULB+QrKXDm4lzUHmHoE2VGGbqtcArF X-Talos-MUID: 9a23:U0xebgrvUKUT0YB/TokezwFBKvpK3KquM1kAtLA6nOS1ECJ0Ix7I2Q== X-IronPort-Anti-Spam-Filtered: true X-IronPort-AV: E=Sophos;i="6.24,200,1774310400"; d="scan'208";a="493780413" Received: from rcdn-l-core-07.cisco.com ([173.37.255.144]) by rcdn-iport-4.cisco.com with ESMTP/TLS/TLS_AES_256_GCM_SHA384; 12 Jun 2026 12:15:22 +0000 Received: from sjc-ads-20495.cisco.com (sjc-ads-20495.cisco.com [171.70.188.248]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits) server-digest SHA256 client-signature RSA-PSS (4096 bits) client-digest SHA256) (Client CN "ciscoit-managed-infra-smtp-auth.cisco.com", Issuer "Internal Private TLS SubCA" (verified OK)) by rcdn-l-core-07.cisco.com (Postfix) with ESMTPS id 49DFF1800023E; Fri, 12 Jun 2026 12:15:22 +0000 (GMT) Received: by sjc-ads-20495.cisco.com (Postfix, from userid 1877012) id E7E36CC1611; Fri, 12 Jun 2026 05:15:21 -0700 (PDT) From: "Ashishkumar Parmar X (asparmar - E INFOCHIPS PRIVATE LIMITED at Cisco)" To: openembedded-core@lists.openembedded.org Cc: xe-linux-external@cisco.com, Ashishkumar Parmar Subject: [OE-core][scarthgap][PATCH 6/6] rsync: Fix CVE-2026-45232 Date: Fri, 12 Jun 2026 05:13:34 -0700 Message-ID: <20260612121514.2282121-6-asparmar@cisco.com> X-Mailer: git-send-email 2.44.1 In-Reply-To: <20260612121514.2282121-1-asparmar@cisco.com> References: <20260612121514.2282121-1-asparmar@cisco.com> MIME-Version: 1.0 X-Auto-Response-Suppress: DR, OOF, AutoReply X-Outbound-Client-TLS: VERIFIED;sjc-ads-20495.cisco.com [171.70.188.248];TLSv1.3;TLS_AES_256_GCM_SHA384;256;ciscoit-managed-infra-smtp-auth.cisco.com X-Outbound-SMTP-Client: 171.70.188.248, sjc-ads-20495.cisco.com X-Outbound-Node: rcdn-l-core-07.cisco.com List-Id: X-Webhook-Received: from 45-33-107-173.ip.linodeusercontent.com [45.33.107.173] by aws-us-west-2-korg-lkml-1.web.codeaurora.org with HTTPS for ; Fri, 12 Jun 2026 12:15:30 -0000 X-Groupsio-URL: https://lists.openembedded.org/g/openembedded-core/message/238609 From: Ashishkumar Parmar 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 --- .../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 +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 +--- + 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"