Fedora Account System
Red Hat Associate
Red Hat Customer
A flaw was found in systemd. The systemd-tmpfiles 'w' item type writes to its target file via write_one_file() in src/tmpfiles/tmpfiles.c, which resolves the parent directory through path_open_parent_safe()/chase() using the CHASE_SAFE mode, then opens the target leaf with O_WRONLY but without O_NOFOLLOW, so a terminal symlink at the target path is followed rather than rejected. Separately, uid_unsafe_transition() in src/basic/chase.c, consulted during the CHASE_SAFE parent-directory walk, treats any transition away from UID 0 as unconditionally safe ('if (a == 0) return false;'), so the walk never flags crossing from a root-owned directory into a directory owned by an unprivileged user. Because /run/user/<uid> is created automatically and owned by that user for any account with an active login session (via logind), an unprivileged local user can place a symlink at a predictable path under their own /run/user/<uid> directory pointing at an arbitrary root-owned file. If a tmpfiles.d configuration containing a 'w'-type entry targets that path and is later processed by a root-privileged systemd-tmpfiles run (at boot, on a timer, or via an administrator- or automation-triggered systemd-tmpfiles --create), the walk into /run/user/<uid> is not flagged as unsafe, write_one_file() follows the attacker's symlink, and root writes that entry's configured content into whichever root-owned file the symlink points to. The content written is determined by the pre-existing tmpfiles.d entry, not freely chosen by the unprivileged user placing the symlink; the unprivileged user controls the destination of the write, not its content, unless they separately have write access to the tmpfiles.d configuration itself. This was reproduced against the systemd package shipped in RHEL 9: an unprivileged test user's symlink under /run/user/<uid> pointing at a root-owned file was followed by systemd-tmpfiles running as root, with the target file's contents overwritten by the configured entry and no warning logged. The specific role of the UID-0 exception was isolated with a comparison case: when the same style of directory-ownership transition occurs between two non-root users instead of from root, systemd-tmpfiles correctly detects and rejects it with an explicit logged warning naming the two users and directories involved, which does not occur in the root-to-unprivileged case. Exploitation requires a tmpfiles.d 'w'-type entry that resolves through attacker-symlinkable space to exist and be processed as root; a systematic audit of every package shipping a tmpfiles.d configuration across RHEL 8, 9, and 10's open package repositories found exactly one such entry (from selinux-policy, targeting a sysfs path), and it does not qualify since its target is not reachable through user-writable directory space.
I'm adding this comment as for per request of Marek Kaminski: Root-cause note for engineering: the specific mechanism reported (uid_unsafe_transition()'s unconditional "a == 0" exception in chase.c, part of the CHASE_SAFE infrastructure) is confirmed present and responsible for this behavior on RHEL 9 and RHEL 10 (systemd 252/257) -- verified via an isolated control test where the identical directory-ownership-transition pattern between two non-root users is correctly detected and blocked with an explicit logged warning, while the root-to-unprivileged case is silently allowed. RHEL 7 and RHEL 8 (systemd 219/239) reproduce the same externally observable behavior (symlink under /run/user/<uid> followed, root-owned target overwritten), but the same isolated control test shows no equivalent transition-safety check firing at all on those releases -- the non-root cross-UID case is also silently allowed there. These older systemd releases predate the CHASE_SAFE/chase.c refactor, so this is likely a broader, pre-existing absence of any such check rather than the same narrow exception. This was not independently confirmed against RHEL 7/8 dist-git source. Practical implication: a fix targeting uid_unsafe_transition() alone (as suggested by the reporter) would address RHEL 9/10 but may not address RHEL 7/8, which may need a different or additional fix depending on what mechanism, if any, they currently rely on for this class of protection.
This was filed in error and has now been retracted. Please close as appropriate. Thanks