Skip to content

syscall: let secure_relative_open() fallback create a missing final component#1034

Open
mabunemeh wants to merge 1 commit into
RsyncProject:masterfrom
mabunemeh:fix-secure-open-enoent-create
Open

syscall: let secure_relative_open() fallback create a missing final component#1034
mabunemeh wants to merge 1 commit into
RsyncProject:masterfrom
mabunemeh:fix-secure-open-enoent-create

Conversation

@mabunemeh

Copy link
Copy Markdown

Fixes #1033

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.

…omponent

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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant