From a5cb5aa669554d2f3cd28cb27fbbdd256746f32e Mon Sep 17 00:00:00 2001 From: Mohammad AbuNemeh Date: Tue, 21 Jul 2026 18:14:29 +0400 Subject: [PATCH] syscall: let secure_relative_open() fallback create a missing final component The per-component O_NOFOLLOW walk fallback in secure_relative_open() -- the tier used when no kernel RESOLVE_BENEATH is available (NetBSD, OpenBSD, Solaris, Cygwin, Linux < 5.6 where openat2 returns ENOSYS, and --disable-openat2 builds) -- probes each component with openat(dirfd, part, O_RDONLY | O_DIRECTORY | O_NOFOLLOW); and only falls back to opening the component as a file when the probe fails with ENOTDIR, i.e. only when the final component already exists as a non-directory. A final component that does not exist yet fails the probe with ENOENT, which is not special-cased, so the walk returns -1/ENOENT: on this tier secure_relative_open() can never create a new file, no matter what flags the caller passed. Impact: since the CVE-2026-29518 hardening the non-chroot daemon receiver routes its --inplace destination open through this helper with O_WRONLY|O_CREAT (receiver.c, secure_basis_open), so every --inplace transfer of a new file into a "use chroot = no" module fails with rsync: [receiver] open "..." failed: No such file or directory (2) and exit code 23 on the fallback tier. The common real-world casualty is MariaDB/Galera rsync SST on RHEL 8 (kernel 4.18, no openat2, distro backport of the same hardening): the joiner datadir is empty, every table file is a create, and the node can never join. Reproducible on any kernel with a --disable-openat2 build: rsync --daemon (use chroot = no) + rsync --inplace -r src/ dst -> fails for every file that does not already exist. Fix: when the O_DIRECTORY probe fails with ENOENT on the LAST component and the caller wants a file (not O_DIRECTORY), open it directly with the caller flags | O_NOFOLLOW, mirroring the existing ENOTDIR last-component fallback. O_CREAT now works; a symlink raced into the name is still refused with ELOOP (O_NOFOLLOW); a missing INTERMEDIATE component (more path follows) still fails with ENOENT; the all-components-were-directories and O_DIRECTORY handling is unchanged, as are the openat2/O_RESOLVE_BENEATH fast paths. --- syscall.c | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/syscall.c b/syscall.c index c7f12d2c2..a89849e33 100644 --- a/syscall.c +++ b/syscall.c @@ -1995,6 +1995,15 @@ int secure_relative_open(const char *basedir, const char *relpath, int flags, mo goto cleanup; } if (next_fd == -1) { + /* Final component that does not exist yet: if the caller + * wants a file (not O_DIRECTORY), open/create it here with + * O_NOFOLLOW so O_CREAT works and a pre-planted symlink at + * the name is still refused. A missing *intermediate* + * component (more path follows) stays a genuine ENOENT. */ + if (errno == ENOENT && !(flags & O_DIRECTORY) + && strtok(NULL, "/") == NULL) { + retfd = openat(dirfd, part, flags | O_NOFOLLOW, mode); + } goto cleanup; } if (dirfd != AT_FDCWD) close(dirfd);