See how systemd compares to other vendors in security performance
In systemd 259 before 260, there is local privilege escalation in systemd-machined because varlink can be used to reach the root namespace.
systemd-networkd is vulnerable to an out out-of-bounds heap write in the DHCPv6 client when handling options sent by network adjacent DHCP servers. A attacker could exploit this via malicious DHCP server to corrupt heap memory on client machines, resulting in a denial of service or potential code execution.
A flaw was found in systemd-journald. A stack buffer overflow when passing several MB of arguments to a program calling syslog function. This can lead to a denial of service attack or arbitrary code execution in some cases.
A flaw was found in systemd-journald. An uncontrolled alloca() by writing a crafted message to /run/systemd/journal/socket that results in a stack buffer overflow. This can lead to a denial of service attack or arbitrary code execution in some cases.
A flaw was found in systemd. The systemd-machined service contains an Improper Access Control vulnerability due to insufficient validation of the class parameter in the RegisterMachine D-Bus (Desktop Bus) method. A local unprivileged user can exploit this by attempting to register a machine with a specific class value, which may leave behind a usable, attacker-controlled machine object. This allows the attacker to invoke methods on the privileged object, leading to the execution of arbitrary commands with root privileges on the host system.
In udev in systemd before 260, local root execution can occur via malicious hardware devices and unsanitized kernel output.
In nspawn in systemd 233 through 259 before 260, an escape-to-host action can occur via a crafted optional config file.
In systemd 260 before 261, a local unprivileged user can trigger an assert via an IPC API call with an array or map that has a null element.
An access control bypass and information disclosure vulnerability exists in the base AppArmor security profile configuration of Canonical snapd. The abstraction rules located in /etc/apparmor.d/abstractions/nss-systemd (inherited via ) inadvertently permit strictly confined snap applications, which lack the privileged account-control interface, to interact directly with the io.systemd.Multiplexer and io.systemd.NameServiceSwitch UNIX domain sockets under /run/systemd/userdb/. On systems where the systemd-userdbd service is installed and operational, the service fails to distinguish between an unconfined root user on the host system and a restricted root user running within a snap application's sandbox (such as a daemon or configuration hook). Because systemd-userdbd returns "complete" user records—including sensitive hashed user passwords from /etc/shadow—when queried by a process running as root, a compromised or malicious strictly confined snap executing code as root can successfully query the Varlink interface to retrieve all system password hashes, bypassing intended snap sandbox restrictions. This issue is mitigated by the fact that systemd-userdbd is not installed by default on standard Ubuntu deployments.
systemd, a system and service manager, (as PID 1) hits an assert and freezes execution when an unprivileged IPC API call is made with spurious data. On version v249 and older the effect is not an assert, but stack overwriting, with the attacker controlled content. From version v250 and newer this is not possible as the safety check causes an assert instead. This IPC call was added in v239, so versions older than that are not affected. Versions 260-rc1, 259.2, 258.5, and 257.11 contain patches. No known workarounds are available.
In systemd 258 before 260, a local unprivileged user can trigger an assert when a Delegate=yes and User=<unset> unit exists and is running.
A race condidition in systemd-coredump allows a local attacker to crash a SUID program and gain read access to the resulting core dump
systemd-coredump sets the sysctl fs.suiddumpable by default to 2 via a sysctl.d drop-in configuration file. For the kernel's builtin coredump handling this setting means that core dumps for setuid (or otherwise privileged) processes will be written to disk but will only be accessible to the root user to avoid sensitive data leaking to unprivileged user accounts. See also man 5 proc for the full documentation of this sysctl.
https://github.com/systemd/systemd/commit/b7641425659243c09473cd8fb3aef2c0d4a3eb9c
A race condidition in systemd-coredump allows a local attacker to crash a SUID program and gain read access to the resulting core dump
It was found that systemd fails an assertion in managerinvokenotifymessage() when a zero-length message is received over its notification socket, causing it to no longer perform it's expected functionality. This issue was assigned CVE-2016-7795 and is tracked via bug 1380286. Upstream bug report is:
https://github.com/systemd/systemd/issues/4234
Older systemd versions either don't include the assert that is triggered in managerinvokenotifymessage(), or have managerprocessnotifyfd() function return error before calling managerinvokenotifymessage(). That error return still causes systemd to exit its main loop and freeze its execution in a similar way it's done in newer versions after failed assertion.
https://github.com/systemd/systemd/issues/4234#issuecomment-250441246
The managerinvokenotifymessage() function with assert was introduced in version v209:
https://github.com/systemd/systemd/commit/5ba6985b6c8ef85a8bcfeb1b65239c863436e75b#diff-ab78220e12703ee63fa1e6a2caa16bebR1319
However, the assertion was not reachable before the error return was removed in v219:
https://github.com/systemd/systemd/commit/d875aa8ce10b458dc218c0d98f4a82c8904d6d03
The systemd versions in Red Hat Enterprise Linux 7.0 and 7.1 are based on upstream v208, but include managerinvokenotifymessage() added via a separate backported patch. The assertion is not reachable, so those versions are affected by CVE-2016-7796, but not affected by CVE-2016-7795. Red Hat Enterprise Linux 7.2 rebased systemd to version v219. Therefore, those packages are no longer affected by CVE-2016-7796, but are now affected by CVE-2016-7795.
systemd, a system and service manager, (as PID 1) hits an assert and freezes execution when an unprivileged IPC API call is made with spurious data. On version v249 and older the effect is not an assert, but stack overwriting, with the attacker controlled content. From version v250 and newer this is not possible as the safety check causes an assert instead. This IPC call was added in v239, so versions older than that are not affected. Versions 260-rc1, 259.2, 258.5, and 257.11 contain patches. No known workarounds are available.
A flaw was found in systemd. The systemd-tmpfiles 'w' item type writes to its target file via writeonefile() in src/tmpfiles/tmpfiles.c, which resolves the parent directory through pathopenparentsafe()/chase() using the CHASESAFE mode, then opens the target leaf with OWRONLY but without ONOFOLLOW, so a terminal symlink at the target path is followed rather than rejected. Separately, uidunsafetransition() in src/basic/chase.c, consulted during the CHASESAFE 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, writeonefile() 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.
A flaw was found in systemd. The systemd-tmpfiles 'w' item type writes to its target file via writeonefile() in src/tmpfiles/tmpfiles.c, which resolves the parent directory through pathopenparentsafe()/chase() using the CHASESAFE mode, then opens the target leaf with OWRONLY but without ONOFOLLOW, so a terminal symlink at the target path is followed rather than rejected. Separately, uidunsafetransition() in src/basic/chase.c, consulted during the CHASESAFE 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, writeonefile() 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.
It was found that systemd fails an assertion in managerinvokenotifymessage when a zero-length message is received over its notification socket. After failing the assertion, PID 1 hangs in the pause system call, making no longer possible to start and stop daemons or cleanly reboot the system. Inetd-style services managed by systemd no longer accept connections.
Since the notification socket, /run/systemd/notify, is world-writable, this allows a local user to perform a denial-of-service attack against systemd.
PoC:
NOTIFYSOCKET=/run/systemd/notify systemd-notify ""
Upstream bug report:
https://github.com/systemd/systemd/issues/4234
CVE request:
http://www.openwall.com/lists/oss-security/2016/09/28/9
A TOCTOU race condition was found in the way the systemd-logind login manager of the systemd, a system and service manager for Linux, performed removal of particular records related with user session upon user logout. A local attacker could use this flaw to conduct symbolic link attacks, potentially leading to removal of arbitrary system file.
A possibility of unauthorized Xorg X11 Server configuration file modification / injection was found in the way SetX11Keyboard() method of systemd, a system and service manager, performed sanitization of provided X Keyboard Extension (XKB) layouts description (special and control characters were not filtered out from the layout description properly). When the host in question used PolicyKit Local Authority (PKLA) file mechanism to grant group of users the privilege to change XKB settings (instead of default PolicyKit check) and particular local attacker was member of that group, they could use this flaw to inject arbitrary values into the Xorg X11 Server configuration file, possibly leading to escalation of their privileges.
Issue found by Florian Weimer, Red Hat Product Security Team
systemd is vulnerable to line splitting via long lines read by fgets() in the unitdeserialize() function during daemon-reexec (e.g. during a package upgrade) allowing for state injection. Systemd services with NotifyAccess != none and malicious executables can exploit this vulnerability resulting corrupted process state.
A stack-based buffer overflow vulnerability was found in getpwnam()/getgrnam() functions of NSS module nss-mymachines provided by systemd.
A flaw was found in systemd-journald. An out-of-bounds read when parsing a crafted syslog message that could lead to information disclosure.
In systemd 259, systemd-journald can send ANSI escape sequences to the terminals of arbitrary users when a "logger -p emerg" command is executed, if ForwardToWall=yes is set.
Qualys Security Advisory
Local information disclosure in apport and systemd-coredump (CVE-2025-5054 and CVE-2025-4598)
======================================================================== Contents ========================================================================
Summary Mitigation Local information disclosure in apport (CVE-2025-5054) - Background - Analysis - Proof of concept Local information disclosure in systemd-coredump (CVE-2025-4598) - Background - Analysis - Proof of concept Acknowledgments Timeline
======================================================================== Summary ========================================================================
We discovered a vulnerability in apport (Ubuntu's core-dump handler), and a similar vulnerability in systemd-coredump (which is the default core-dump handler on Red Hat Enterprise Linux 9 and Fedora for example): a race condition that allows a local attacker to crash a SUID program and gain read access to the resulting core dump (by quickly replacing the crashed SUID process with another process, before its /proc/pid/ files are analyzed by the vulnerable core-dump handler).
We developed two proofs of concepts for these vulnerabilities (one for Ubuntu 24.04, and one for Fedora 40 and 41, but other distributions are probably also vulnerable and exploitable): they allow a local attacker to obtain the contents of /etc/shadow (password hashes) from the core dump of a crashed unixchkpwd process (unixchkpwd is a SUID or SGID program that is installed by default on most Linux distributions).
Last-minute update: while working on these vulnerabilities, we eventually realized that systemd-coredump does not specify %d (the kernel's per-process "dumpable" flag) in /proc/sys/kernel/corepattern; consequently a local attacker can crash (with kill(SIGSEGV) for example) root daemons that fork() and setuid() to the attacker's uid, gain read access to the resulting core dumps, and therefore to the root daemons' memory. For example, we wrote a trivial proof of concept that dumps the memory of OpenSSH's sshd-session, systemd's sd-pam, and the cron daemon, and obtained secret information such as half of sshd's private ed25519 host key, password hashes from /etc/shadow, other users' crontabs, ASLR addresses, stack canaries. This second attack (against root daemons) is powerful, different from the first attack (against SUID programs), and can certainly be further improved; and other secrets can certainly be obtained from other daemons, but this is left as an exercise for the interested reader.
The fix for these vulnerabilities is twofold:
- always take account of the kernel's per-process "dumpable" flag (the %d specifier), in every code path, to decide whether a non-root user should be given read access to a core dump or not;
- use the new %F specifier in /proc/sys/kernel/corepattern (a pidfd to the crashed process), which was implemented during this coordinated vulnerability disclosure, to detect whether the crashed process was replaced or not with another process, before its analysis; for more information:
https://lore.kernel.org/all/20250414-work-coredump-v2-0-685bf231f828 () kernel org/
======================================================================== Mitigation ========================================================================
To mitigate these vulnerabilities, /proc/sys/fs/suiddumpable can be set to 0 (SUIDDUMPDISABLE, "No setuid dumping"). This prevents all SUID programs and root daemons that drop privileges from being analyzed in case of a crash, but it can act as a temporary fix if the vulnerable core-dump handler itself cannot be patched immediately.
======================================================================== Local information disclosure in apport (CVE-2025-5054) ========================================================================
------------------------------------------------------------------------ Background ------------------------------------------------------------------------
After our discovery of three bypasses in Ubuntu's unprivileged user namespace restrictions, we decided to look for a real-world example of a vulnerability that requires a user namespace with full capabilities. One perfectly obvious example would be a kernel vulnerability that requires CAPSYSADMIN or CAPNETADMIN, but finding and exploiting such a kernel vulnerability would most likely take us months, so we decided to look for a simple userland vulnerability instead.
One target that immediately came to mind is apport, Ubuntu's core-dump handler, because it suffered from multiple vulnerabilities related to namespaces (containers) in the past; for example, the following excellent write-ups by Tavis Ormandy and Sander Bos:
- CVE-2015-1318: https://www.openwall.com/lists/oss-security/2015/04/14/4 - CVE-2017-14180: https://bugs.launchpad.net/ubuntu/+source/apport/+bug/1726372 - CVE-2019-11483: https://bugs.launchpad.net/apport/+bug/1839420
But as soon as we started to read apport's source code, we realized that it has been considerably hardened over the years:
- The most common attack vector against apport, which consisted in tricking apport into dumping an attacker-controlled core file into a root-owned directory such as /etc/sudoers.d/ or /etc/logrotate.d/, has been completely eradicated: apport now dumps all core files into a hard-coded directory (/var/lib/apport/coredump/ by default).
- The race condition that allows a local attacker to replace a crashed process with another process, before its /proc/pid/ files are analyzed but after apport has started, has been largely mitigated in apport (by thorough security checks in its consistencychecks() function).
To further detail this last point: perhaps surprisingly, a local attacker can send a SIGKILL signal to an already-crashed process, thus allowing the attacker to recycle the crashed process's pid (by creating many new processes until the crashed-and-killed process's pid is reused) and tricking apport into analyzing the /proc/pid/ files of the wrong process. This race condition has been exploited several times in the past; for example, the following outstanding write-ups by Philip Pettersson, Kevin Backhouse, Ryota Shiga, and Itai Greenhut:
- CVE-2015-1325: https://www.openwall.com/lists/oss-security/2015/05/21/10 - CVE-2019-15790: https://github.blog/security/vulnerability-research/ubuntu-apport-pid-recycling-security-vulnerability-cve-2019-15790/ - CVE-2020-15702: https://flatt.tech/research/posts/race-condition-vulnerability-in-handling-of-pid-by-apport/ - CVE-2021-25684: https://alephsecurity.com/2021/02/16/apport-lpe/
But as mentioned earlier, this race condition has now been largely mitigated in apport, by:
- immediately open()ing a file descriptor to the crashed process's /proc/pid/ directory and accessing all the files in this directory through this file descriptor and the at() syscalls (openat() etc);
- checking that the starttime in /proc/pid/stat is earlier than the starttime of apport itself (i.e., ensuring that an attacker has not replaced the crashed process with another process, after apport has started);
- double-checking that the real Uid and Gid in /proc/pid/status still match the real uid and gid of the crashed process at the time of its crash.
Last-minute note: we eventually verified that the starttime check is in fact useless, from a security point of view; an attacker can replace the crashed process with another process even before apport starts, and with the right timing can still give the kernel enough time to generate the core dump of the originally crashed process.
------------------------------------------------------------------------ Analysis ------------------------------------------------------------------------
Unfortunately, while reading apport's code we noticed that the function that handles crashes inside namespaces (checkglobalpidandforward(), at line 769) is called before the aforementioned security checks are run (in consistencychecks(), at line 951); in other words, an attacker can trick apport's checkglobalpidandforward() into analyzing the wrong process, while the kernel still sends the core dump of the originally crashed process to apport (over its file descriptor 0, stdin):
------------------------------------------------------------------------ 750 def main(args: list[str]) -> int: ... 769 if checkglobalpidandforward(options): 770 return 0 ... 775 return processcrashfromkernel(options) ------------------------------------------------------------------------ 921 def processcrashfromkernel(options: argparse.Namespace) -> int: ... 924 return processcrashfromkernelwithprocpid(options, procpid) ------------------------------------------------------------------------ 941 def processcrashfromkernelwithprocpid( 942 options: argparse.Namespace, procpid: ProcPid 943 ) -> int: ... 951 if not consistencychecks(options, processstart, procpid, realuser): 952 return 0 ------------------------------------------------------------------------
And so an attack idea against apport began to form in our mind:
a/ first, we fork() a new process and execve() a SUID or SGID program, and wait until it loads secret information into its memory (for example, password hashes from /etc/shadow);
b/ second, we crash this process at the right time, by kill()ing it with a core-dumping signal such as SIGSEGV or SIGSYS, thus causing the kernel to create a new apport process to analyze this crash;
c/ then, after apport has started but before it analyzes the crashed process's /proc/pid/ files, we SIGKILL the crashed process and quickly replace it with another process that is not SUID or SGID, but that is running inside a user, mount, and pid namespace (to pass the tests at lines 726-727, below);
(note: naturally, we use one of our bypasses in Ubuntu's unprivileged user namespace restrictions to create this namespace)
d/ as a result, apport connects to the Unix socket /run/apport.socket inside our mount namespace (at lines 521-584, below) and sends us its file descriptor 0, from where we can read the kernel-generated core dump of the originally crashed process, and hence the secret information from the memory of the SUID or SGID program (for example, password hashes).
------------------------------------------------------------------------ 712 def checkglobalpidandforward(options: argparse.Namespace) -> bool: ... 726 if not issamens(options.globalpid, "mnt"): 727 if not issamens(options.globalpid, "pid"): 728 forwardcrashtocontainer(options) 729 return True ------------------------------------------------------------------------ 509 def forwardcrashtocontainer( 510 options: argparse.Namespace, coredumpfd: int = 0, hascapsysadmin: bool = True 511 ) -> None: ... 521 prochostpidfd = os.open( 522 f"/proc/{options.globalpid}", os.ORDONLY | os.OPATH | os.ODIRECTORY 523 ) ... 531 sockfd = os.open( 532 "root/run/apport.socket", os.ORDONLY | os.OPATH, dirfd=prochostpidfd 533 ) ... 584 sock.connect(f"/proc/self/fd/{sockfd}") ------------------------------------------------------------------------
To put this theoretical attack idea into practice, we must solve three major problems:
1/ In step c/ we must SIGKILL the crashed process long before we can read any information from the file descriptor 0 that apport sends to us. This file descriptor 0 is the read end of a pipe whose internal 64KB buffer is filled by the kernel with the beginning of the crashed process's core dump, before we SIGKILL it.
The question, then, is: can we find a SUID or SGID program whose ELF segments and heap fit into the pipe's internal 64KB buffer, and whose heap contains secret information?
Luckily we found unixchkpwd, a small (~31KB) SUID-root or SGID-shadow program that is used by PAM to verify the password of a user, and which therefore loads the contents of /etc/shadow (password hashes) into its heap.
Last-minute note: while drafting this advisory, we realized that it might be possible to use /proc/pid/coredumpfilter to exclude the ELF segments from the program's core dump, which might make it possible to attack larger SUID programs such as su or sudo; this is left as an exercise for the interested reader.
2/ In step b/ we must win a first race condition: we must crash the SUID process "at the right time", with a SIGSEGV or SIGSYS for example. If we crash it too early, then the password hashes from /etc/shadow are not loaded into the heap yet; if we crash it too late, then the password hashes in the heap are already overwritten with other information.
Ideally, to reliably win this race condition, we should add an INCLOSENOWRITE watch on /etc/shadow, which would allow us to crash the SUID process as soon as the contents of /etc/shadow are loaded into its heap. Unfortunately, we cannot add such a watch, because /etc/shadow is not readable by us. As a makeshift solution, we add an INCLOSENOWRITE watch on /etc/passwd instead, which is opened and closed immediately before /etc/shadow.
With our proof of concept, we almost always obtain some password hashes from unixchkpwd's heap, and from time to time we also obtain the entire contents of /etc/shadow. In any case, we can simply re-execute our proof of concept until we obtain the password hash that we are looking for, and we believe that the reliability of this step b/ can still be improved.
3/ In step c/ we must win a second race condition: we must SIGKILL the crashed SUID process and "quickly" replace it with a non-SUID namespaced process (before apport calls its checkglobalpidandforward()). If we SIGKILL it too early, then the kernel does not have enough time to write the beginning of the crashed process's core dump into apport's file descriptor 0; if we SIGKILL it too late, then apport analyzes the crashed process's /proc/pid/ (instead of our namespaced process's /proc/pid/) and therefore does not send us its file descriptor 0.
In our experiments, and depending on the test machines, it takes between 1 and 4 minutes to replace the crashed SUID process with another process (i.e., to recycle its pid) because /proc/sys/kernel/pidmax is 4M (2^22) nowadays, not 32K (note: we call clone() with most of the CLONE flags to create new processes, to minimize the work done by the kernel; it would take 3 to 6 times longer if we were simply calling fork()).
Consequently, we cannot just "quickly" replace the crashed SUID process in step c/; instead:
- in step b/ we do not immediately crash the SUID process (with SIGSEGV or SIGSYS), but we first SIGSTOP it, then create ~4M processes until their pids wrap around and almost reach the pid of the SUID process, and finally we crash (SIGSEGV or SIGSYS) and resume (SIGCONT) the SUID process;
- in step c/ we SIGKILL the crashed SUID process, and quickly create a mere handful of namespaced processes until their pids reach and reuse the pid of the crashed-and-killed SUID process.
Our proof of concept always wins this second race condition (the "kill-and-replace" race condition): because apport is written in Python, it loads numerous .pyc files during its initialization, so we simply add an INOPEN watch on one of these files (on aptdpkg.cpython-312.pyc for example) and still have plenty of time (after apport has triggered our watch point) to SIGKILL and replace the crashed SUID process with a namespaced process (before apport analyzes its /proc/pid/ files).
------------------------------------------------------------------------ Proof of concept ------------------------------------------------------------------------
$ grep PRETTYNAME= /etc/os-release PRETTYNAME="Ubuntu 24.04.2 LTS"
$ id uid=1001(evey) gid=1001(evey) groups=1001(evey),100(users)
$ while true; do core="$(printf 'whatever\0' | ./CVE-2025-5054 /usr/sbin/unixchkpwd "$USER" nullok)"; if tr -c ' -~' '\n' < "$core" | grep '\$[0-9A-Za-z]\+\$[0-9A-Za-z./]'; then break; fi; done
... pid 1093 tid 1030 core will be dumped in /tmp/run.q5qBcg signal 9 accept 4 args 1093 31 18446744073709551615 2 fd 5 died in childuserns: 151 status 1 died in main: 280 $y$j9T$KC0.pKjUYzrr3L8VVNQ8l/$11KufHkbNKHRxgolryPxDQDZ.Ox9kG4RIv0Pxe1FgxA $y$j9T$KC0.pKjUYzrr3L8VVNQ8l/$11KufHkbNKHRxgolryPxDQDZ.Ox9kG4RIv0Pxe1FgxA theadmin:$6$7Ag0AvjQl4XQvSO4$T1mMcQeC0K7FICHEj9pNV20XcUX4IW6Xqg45lyuORtia1vPCOy2ZrFlTa.ZEf0EAO6rpNRma1ucCjO3aL64KW0:20145:0:99999:7::: evey:$y$j9T$KC0.pKjUYzrr3L8VVNQ8l/$11KufHkbNKHRxgolryPxDQDZ.Ox9kG4RIv0Pxe1FgxA:20145:0:99999:7:::
======================================================================== Local information disclosure in systemd-coredump (CVE-2025-4598) ========================================================================
------------------------------------------------------------------------ Background ------------------------------------------------------------------------
While working on Ubuntu's apport, we remembered that various other distributions (Red Hat Enterprise Linux 9 and Fedora for example) use systemd-coredump as a core-dump handler in /proc/sys/kernel/corepattern (instead of apport). We began to wonder: how does systemd-coredump solve the kill-and-replace race condition that we exploited against apport?
Similarly to apport, systemd-coredump writes all core files into a hard-coded directory, /var/lib/systemd/coredump/. Before December 2022, systemd-coredump allowed users to read all of their core files (through file ACLs), including the core files of SUID or SGID programs, which of course allowed local attackers to read the contents of /etc/shadow by simply crashing su for example; this vulnerability was CVE-2022-4415, discovered and published by Matthias Gerstner:
https://www.openwall.com/lists/oss-security/2022/12/21/3
This old vulnerability was patched by introducing a new function, grantuseraccess(), which decides whether a user should be allowed to read a core file or not, by analyzing the /proc/pid/auxv of the crashed process: if its ATUID and ATEUID match, and if its ATGID and ATEGID match, and if its ATSECURE flag is 0, then read access is allowed; otherwise (if the crashed process is SUID or SGID), read access is denied (only root can read the core file).
------------------------------------------------------------------------ Analysis ------------------------------------------------------------------------
Unfortunately, we soon realized that systemd-coredump does not provide any protection at all against the kill-and-replace race condition that we exploited in apport. In other words, an attacker can simply crash a SUID process such as unixchkpwd, SIGKILL and replace it with a non-SUID process (before its /proc/pid/auxv is analyzed by systemd-coredump), and therefore gain read access to the core file of the crashed SUID process, and hence to the contents of /etc/shadow.
On the one hand, exploiting systemd-coredump is easier than exploiting apport, because we do not need to replace the crashed SUID process with a namespaced process: we can replace it with any non-SUID process, whose ATUID and ATEUID match, whose ATGID and ATEGID match, and whose ATSECURE flag is 0.
On the other hand, winning the kill-and-replace race condition against systemd-coredump is harder: unlike apport, systemd-coredump is written in C, and its initialization takes little time. To widen the window of the race condition, we pass an argv[0] of 128K '\177' characters to the SUID process: this slows down the analysis of its /proc/pid/cmdline (by systemd-coredump, before the analysis of its /proc/pid/auxv) and gives us enough time to replace the crashed SUID process with a non-SUID process.
------------------------------------------------------------------------ Proof of concept ------------------------------------------------------------------------
$ grep PRETTYNAME= /etc/os-release PRETTYNAME="Fedora Linux 41 (Server Edition)"
$ id uid=1001(evey) gid=1001(evey) groups=1001(evey) context=unconfinedu:unconfinedr:unconfinedt:s0-s0:c0.c1023
$ while true; do pid="$(printf 'whatever\0' | ./CVE-2025-4598 /usr/sbin/unixchkpwd "$USER" nullok)"; pidwait -f /usr/lib/systemd/systemd-coredump; if coredumpctl -1 dump "$pid" 2>/dev/null | strings -a | grep '\$[0-9A-Za-z]\+\$[0-9A-Za-z./]'; then break; fi; done
... pid 364536 tid 364521 tid 364540 died in main: 177 theadmin:$y$j9T$APKdqQO.brzhEbC2JFd.5zb7$Rz2q.0umBr8AmkwlozWr8/yphm/ckEHIOMo9vcj.Wj/::0:99999:7::: evey:$y$j9T$QUW3HEErO9CYuGrRhiQjt.$.befySFW/nA48280u/Hk1XrcA2yDZ6Z1s7iRf91nJuA:20188:0:99999:7:::
======================================================================== Acknowledgments ========================================================================
We thank Ubuntu's security team and apport's developers (Octavio Galland and Benjamin Drung in particular), and systemd's developers (Zbigniew Jedrzejewski Szmek and Luca Boccassi in particular), for their hard work on this release. We also thank Red Hat Product Security (Marco Benatto in particular), and the linux-distros@openwall (Solar Designer, Seth Arnold, Salvatore Bonaccorso, David Fernandez Gonzalez, in particular), for their help with this release. Finally, we thank Christian Brauner for the %F/pidfd kernel feature.
======================================================================== Timeline ========================================================================
2025-03-21: We sent a draft of our advisory and a first proof of concept (against unixchkpwd) to Ubuntu's security team.
2025-04-10: We sent a draft of our advisory and a first proof of concept (against unixchkpwd) to systemd's developers.
2025-04-17: We sent a second proof of concept (against sshd) to systemd's developers.
2025-04-22: We sent a second proof of concept (which defeats apport's starttime check) to Ubuntu's security team and apport's developers.
2025-05-23: We sent a draft of our advisory to the linux-distros@openwall.
2025-05-29: Coordinated Release Date (16:00 UTC).
Hi Zbigniew,
On Thu, May 29, 2025 at 05:17:08PM +0000, Qualys Security Advisory wrote: - always take account of the kernel's per-process "dumpable" flag (the %d specifier), in every code path, to decide whether a non-root user should be given read access to a core dump or not; Thank you for fixing the issue in systemd, and for sharing patches with linux-distros. I have a question on those. Since this is now public, let's discuss this in the public oss-security thread.
In your message to linux-distros, you shared these two patches:
0001-coredump-get-rid-of-METAMANDATORYMAX.patch 0003-coredump-also-stop-forwarding-non-dumpable-processes.patch
So it looks like you omitted patch number 2. Yet to me that omitted patch would have been the most important part of the fix. Was this omission inadvertent, or am I missing some reason to skip that patch?
I think it's these 3 commits (as they appear in the main branch, and I see equivalent ones are also in v257-stable and v256-stable):
commit 8fc7b2a211eb13ef1a94250b28e1c79cab8bdcb9 Author: Zbigniew Jędrzejewski-Szmek <zbyszek () in waw pl> Date: Mon May 5 15:48:40 2025 +0200
coredump: also stop forwarding non-dumpable processes See the comment in the patch for details. Suggested-by: Qualys Security Advisory <qsa () qualys com>
commit 0c49e0049b7665bb7769a13ef346fef92e1ad4d6 Author: Zbigniew Jędrzejewski-Szmek <zbyszek () in waw pl> Date: Tue Apr 29 14:47:59 2025 +0200
coredump: use %d in kernel core pattern The kernel provides %d which is documented as "dump mode—same as value returned by prctl(2) PRGETDUMPABLE". We already query /proc/pid/auxv for this information, but unfortunately this check is subject to a race, because the crashed process may be replaced by an attacker before we read this data, for example replacing a SUID process that was killed by a signal with another process that is not SUID, tricking us into making the coredump of the original process readable by the attacker. With this patch, we effectively add one more check to the list of conditions that need be satisfied if we are to make the coredump accessible to the user. Reportedy-by: Qualys Security Advisory <qsa () qualys com> In principle, %d might return a value other than 0, 1, or 2 in the future. Thus, we accept those, but emit a notice.
commit 49f1f2d4a7612bbed5211a73d11d6a94fbe3bb69 Author: Zbigniew Jędrzejewski-Szmek <zbyszek () in waw pl> Date: Mon May 26 12:04:44 2025 +0200
coredump: get rid of METAMANDATORYMAX No functional change. This change is done in preparation for future changes. Currently, the list of fields which are received on the command line is a strict subset of the fields which are always expected to be received on a socket. But when we add new kernel args in the future, we'll have two non-overlapping sets and this approach will not work. Get rid of the variable and enumerate the required fields. This set will never change, so this is actually more maintainable. The message with the hint where to add new fields is switched with METAARGVMAX. The new order is more correct.
Alexander
This information is to be publicly released on January 6 per requirements of the distro list.
This most likely impacts all recent VMs on most modern hypervisors.
Thanks,
Greg Dahlman
Overview
Systemd v256 change - When the openssh-server package is installed on a VM with vsock support, systemd now automatically starts an sshd instance that listens on the afvsock socket in the global network namespace without any manual configuration.
vsock exists in the global namespace - Unlike "afinet" sockets, vsock connections are not bound to a particular network namespace. By default they are visible to every namespace on the host.
Violation of namespace isolation - Users normally expect that services bound in one namespace cannot be accessed from another namespace. The global‑namespace vsock listener breaks this expectation, allowing processes in any namespace to reach the sshd instance.
Enables malware and lateral movement - Malicious code that runs inside a container or sandbox can exploit the exposed vsock listener to connect to the host’s SSH daemon, thereby bypassing network‑segmentation rules and moving laterally across the host. This creates a powerful attack vector for malware that can spread from isolated workloads to the host or other guests without needing traditional network exposure.
Hard‑to‑audit data path - vsock provides a low‑level, kernel‑backed IPC channel that is opaque to many security tools. It can be used by sandboxed programs or containers to send commands or data to sandboxed programs or containers in a way that is difficult to monitor or audit.
vsock ss/netstat invisibility - The visibility feature is isolated in a network namespace, letting processes evade detection in an already hard‑to‑audit subsystem.
Trivial extension of active threats - If not already being leveraged, it would be trivial to extend [BRICKSTORM] and [shai- hulud] to take advantage of vsock as described above. As [BRICKSTORM] is already leveraging vsock on VmWare, it is unlikely it is not already being used by advanced threats.
[BRICKSTORM] https://www.cisa.gov/news-events/analysis-reports/ar25-338a#AppC
[shai-hulud] https://www.wiz.io/blog/shai-hulud-2-0-aftermath-ongoing-supply-chain-attack
What did systemd say for the malicious vectors on this change?
Dec 28, 2025, 12:20 by dahlmanatgmail.comyen-mummify-yeah () duck com: This information is to be publicly released on January 6 per requirements of the distro list. This most likely impacts all recent VMs on most modern hypervisors. Thanks, Greg Dahlman Overview DuckDuckGo> did not detect any trackers. > > More <https://duckduckgo.com/-yPPlCVssOmY70ZnFvF-Wddd1QVblRSWUzjDgQW0TwaWlOck8n8Ygc4uUWFOC0MIJjOCjbYQbaDnBbkZETdwzuTGuVfdqEg6gB0ZExR5xaWYrVcTRoiFA6TclKbZwAFTyPnXg5X0PS0OyyEtjYQBJHEzpeSU-hRarcRIWDBFrNec0XCuV8O59Dplp9litlpyij8AzA8uvCO2VQI07SH4enlMeH4OCVIQSCgUfYvHtKDDZ9v0NuPkhurpI4yN5xx-Ac> Unable to verify sender identity Deactivate <https://duckduckgo.com/> This information is to be publicly released on January 6 per requirements of the distro list.
This most likely impacts all recent VMs on most modern hypervisors.
Thanks,
Greg Dahlman
Overview
Systemd v256 change - When the openssh-server package is installed on a VM with vsock support, systemd now automatically starts an sshd instance that listens on the afvsock socket in the global network namespace without any manual configuration.
vsock exists in the global namespace - Unlike "afinet" sockets, vsock connections are not bound to a particular network namespace. By default they are visible to every namespace on the host.
Violation of namespace isolation - Users normally expect that services bound in one namespace cannot be accessed from another namespace. The global‑namespace vsock listener breaks this expectation, allowing processes in any namespace to reach the sshd instance.
Enables malware and lateral movement - Malicious code that runs inside a container or sandbox can exploit the exposed vsock listener to connect to the host’s SSH daemon, thereby bypassing network‑segmentation rules and moving laterally across the host. This creates a powerful attack vector for malware that can spread from isolated workloads to the host or other guests without needing traditional network exposure.
Hard‑to‑audit data path - vsock provides a low‑level, kernel‑backed IPC channel that is opaque to many security tools. It can be used by sandboxed programs or containers to send commands or data to sandboxed programs or containers in a way that is difficult to monitor or audit.
vsock ss/netstat invisibility - The visibility feature is isolated in a network namespace, letting processes evade detection in an already hard‑to‑audit subsystem.
Trivial extension of active threats - If not already being leveraged, it would be trivial to extend [BRICKSTORM] and [shai- hulud] to take advantage of vsock as described above. As [BRICKSTORM] is already leveraging vsock on VmWare, it is unlikely it is not already being used by advanced threats.
[BRICKSTORM] > https://www.cisa.gov/news-events/analysis-reports/ar25-338a#AppC
[shai-hulud] > https://www.wiz.io/blog/shai-hulud-2-0-aftermath-ongoing-supply-chain-attack
The official way to disable this behavior requires appending "systemd.sshauto=no" to the kernel boot line.
https://man7.org/linux/man-pages/man8/systemd-ssh-generator.8.html
On Sun, Dec 28, 2025 at 9:11 PM Jacob Bachmeyer <jcb62281 () gmail com> wrote: On 12/27/25 21:46, Greg Dahlman wrote: [...]
Systemd v256 change - When the openssh-server package is installed on a VM with vsock support, systemd now automatically starts an sshd instance that listens on the afvsock socket in the global network namespace without any manual configuration. Obvious question: what manual configuration is required to kill that listener?
-- Jacob