| by Arround The Web | No comments

GNU Guix: Privilege Escalation Vulnerabilities (CVE-2025-46415, CVE-2025-46416)

Two security issues, known as
CVE-2025-46415 and
CVE-2025-46416, have been
identified in
guix-daemon,
which allow for a local user to gain the privileges of any of the build users
and subsequently use this to manipulate the output of any build, as well as to
subsequently gain the privileges of the daemon user. You are strongly advised
to upgrade your daemon now (see instructions below), especially on
multi-user systems.

Both exploits require the ability to start a derivation build. CVE-2025-46415
requires the ability to create files in /tmp in the root mount namespace on
the machine the build occurs on, and CVE-2025-46416 requires the ability to run
arbitrary code in the root PID and network namespaces on the machine the build
occurs on. As such, this represents an increased risk primarily to multi-user
systems, but also more generally to any system in which untrusted code may be
able to access guix-daemon's socket, which is usually located at
/var/guix/daemon-socket/socket.

Vulnerability

One of the longstanding oversights of Guix's build environment isolation is what
has become known as the abstract Unix-domain socket hole: a Linux-specific
feature that enables any two processes in the same network namespace to
communicate via Unix-domain sockets, regardless of all other namespace
state
. Unix-domain sockets are perhaps the single most powerful form of
interprocess communication (IPC) that Unix-like systems have to offer, for the
reason that they allow file descriptors to be passed between processes.

This behavior had played a crucial role in
CVE-2024-27297,
in which it was possible to smuggle a writable file descriptor to one of the
output files of a fixed-output
derivation

to a process outside of the build environment sandbox. More specifically, this
would use a fixed-output derivation that doesn't use a builtin builder; examples
of this class of derivation include derivations produced by origins using
svn-fetch and hg-fetch, but not git-fetch or url-fetch, since those are
implemented using builtin builders. The process could then wait for the daemon
to validate the hash and register the output, and subsequently modify the file
to contain any contents it desired.

The fix for CVE-2024-27297 seems to have made the assumption that once the build
was finished, no more processes could be running as that build user. This is
unfortunately incorrect: the builder could also smuggle out the file descriptor
of a setuid program, which could subsequently be executed either using
/proc/self/fd/N or execveat to gain the privileges of the build user. This
assumption was likely believed to hold in Nix because Nix had a seccomp filter
that attempted to forbid the creation of setuid programs entirely by blocking
the necessary chmod calls. The security researchers who discovered
CVE-2025-46415 and CVE-2025-46416 discovered ways around Nix's seccomp filter,
but Guix never had any such filter to begin with. It was therefore possible to
run arbitrary code as the build user outside of the isolated build environment
at any time.

Because it is possible to run arbitrary code as the build user even after the
build has finished, many assumptions made in the design of the build daemon —
not only in fixing CVE-2024-27297 but going way back — can be violated and
exploited. One such assumption is that directories being deleted by
deletePath — for instance the build tree of a build that has just failed —
won't be modified while it is recursing through them. By violating this
assumption, it is possible to exploit race conditions in deletePath to get the
daemon to delete arbitrary files. One such file is a build directory of the
form /tmp/guix-build-PACKAGE-X.Y.drv-0. If this is done between when the
build directory is created and when it is chowned to the build user, an
attacker can put a symbolic link in the appropriate place and get it to chown
any file owned by the daemon's user to now be owned by the build user. In the
case of a daemon running as root, that includes files such as /etc/passwd.
The build users, as mentioned before, are easily compromised, so an attacker can
at this point write to the target file.

When guix-daemon is not running as
root
,
the attacker would gain privileges of the guix-daemon user, giving write
access to the store and nothing else.

In short, there are two separate problems here:

  1. It is possible to take over build users by exfiltrating setuid programs
    (CVE-2025-46416).
  2. Race conditions in the daemon make it possible to elevate privileges when
    other processes can concurrently modify files it operates on
    (CVE-2025-46415).

Mitigation

This security issue has been fixed by 6 commits
(7173c2c0ca,
be8aca0651,
fb42611b8f,
c659f977bb,
0e79d5b655,
and
30a5d140aa
as part of pull request
#788
). Users should make
sure they have upgraded to commit
30a5d140aa
or any later commit to be protected from this vulnerability. Upgrade
instructions are in the following section.

The fix was accomplished primarily by closing the "abstract Unix-domain socket
hole" entirely. To do this, the daemon was modified so that all builds — even
fixed-output ones – occur in a fresh network
namespace
. To
keep networking functional despite the separate network namespace, a userspace
networking stack,
slirp4netns, is used.
Additionally, some of the daemon's file deletion and copying helper procedures
were modified to use the openat family of system calls, so that even in cases
where build users can be taken over (for example, when the daemon is run with
--disable-chroot), those particular helper procedures can't be exploited to
escalate privileges.

A test for the presence of the abstract Unix-domain socket hole is available at
the end of this post. One can run this code with:

guix repl -- abstract-socket-vuln-check.scm

This will output whether the current guix-daemon being used is vulnerable or
not. If it is not vulnerable, the last line will contain Abstract unix socket hole is CLOSED, otherwise the last line will contain Abstract unix socket hole is OPEN, guix-daemon is VULNERABLE.

Note that this will properly report that the hole is still open for daemons
running with --disable-chroot, which is, as before, still insecure wherever
untrusted users can access the daemon's socket.

Upgrading

Due to the severity of this security advisory, we strongly recommend
all users to upgrade guix-daemon immediately
.

For Guix System, the
procedure

is to reconfigure the system after a guix pull, either restarting
guix-daemon or rebooting. For example:

guix pull
sudo guix system reconfigure /run/current-system/configuration.scm
sudo herd restart guix-daemon

where /run/current-system/configuration.scm is the current system
configuration but could, of course, be replaced by a system
configuration file of a user's choice.

For Guix on another distribution, one
needs to guix pull with sudo, as the guix-daemon runs as root,
and restart the guix-daemon service, as
documented
.
For example, on a system using systemd to manage services, run:

sudo --login guix pull
sudo systemctl restart guix-daemon.service

Note that for users with their distro's package of Guix (as opposed to
having used the install
script
)
you may need to take other steps or upgrade the Guix package as per
other packages on your distro. Please consult the relevant
documentation from your distro or contact the package maintainer for
additional information or questions.

Timeline

On March 27th, the NixOS/Nixpkgs security team forwarded a detailed report about
two vulnerabilities from Snyk Security Labs to the Guix security
team
and to Ludovic Courtès and Reepca
Russelstein (as contributors to guix-daemon). A 90-day disclosure timeline
was agreed upon with Snyk and all the affected projects: Nix, Lix, and Guix.

During that time, development of the fixes in Guix was led by Reepca Russelstein
with peer review happening on the private guix-security mailing list.
Coordination with the other projects and for this security advisory was managed
by the Guix security team.

A pre-disclosure announcement was sent by the
NixOS/Nixpkgs
and the
Guix
security teams on June 19th–20th, giving June 24th as the full public disclosure
date.

Some other CVEs that were included in the report were CVE-2025-52991,
CVE-2025-52992, and CVE-2025-52993. These don't represent direct
vulnerabilities so much as missed opportunities to mitigate the attack the
researchers identified — that is, it has to be possible to do things like
exfiltrate file descriptors (for CVE-2025-52992) and trick the daemon into
deleting arbitrary files (for CVE-2025-52991 and CVE-2025-52993) before these
start mattering.

Conclusion

More information concerning the fix for this vulnerability and the design
choices made for it will be provided in a follow-up blog post.

We thank the Security Labs team at Snyk for discovering
similar-but-not-quite-the-same vulnerabilities in Nix, and the NixOS/Nixpkgs
security team for sharing this information with the Guix security team, which
led us to realize our own related vulnerabilities.

Test for presence of vulnerability

Below is code to check if your guix-daemon is vulnerable to this exploit. Save
this file as abstract-socket-vuln-check.scm and run following the instructions
above, in "Mitigation."

;; Checking for CVE-2025-46415 and CVE-2025-46416.

(use-modules (guix)
             (gcrypt hash)
             ((rnrs bytevectors) #:select (string->utf8))
             (ice-9 match)
             (ice-9 threads)
             (srfi srfi-34))

(define nonce
  (string-append "-" (number->string (car (gettimeofday)) 16)
                 "-" (number->string (getpid))))

(define socket-name
  (string-append "\0" nonce))

(define test-message nonce)

(define check
  (computed-file
   "check-abstract-socket-hole"
   #~(begin
       (use-modules (ice-9 textual-ports))

       (let ((sock (socket AF_UNIX SOCK_STREAM 0)))
         ;; Attempt to connect to the abstract Unix-domain socket outside.
         (connect sock AF_UNIX #$socket-name)

         ;; If we reach this line, then we successfully managed to connect to
         ;; the abstract Unix-domain socket.
         (call-with-output-file #$output
           (lambda (port)
             (display (get-string-all sock) port)))))
   #:options
   `(#:hash-algo sha256
     #:hash ,(sha256 (string->utf8 test-message))
     #:local-build? #t)))

(define build-result
  ;; Listen on the abstract Unix-domain socket at SOCKET-NAME and build
  ;; CHECK.  If CHECK succeeds, then it managed to connect to SOCKET-NAME.
  (let ((sock (socket AF_UNIX SOCK_STREAM 0)))
    (bind sock AF_UNIX socket-name)
    (listen sock 1)
    (call-with-new-thread
     (lambda ()
       (match (accept sock)
         ((connection . peer)
          (format #t "accepted connection on abstract Unix-domain socket~%")
          (display test-message connection)
          (close-port connection)))))
    (with-store store
      (let ((drv (run-with-store store (lower-object check))))
        (guard (c ((store-protocol-error? c) c))
          (build-derivations store (list drv))
          #t)))))

(if (store-protocol-error? build-result)
    (format (current-error-port)
            "Abstract Unix-domain socket hole is CLOSED, build failed with ~S.~%"
            (store-protocol-error-message build-result))
    (format (current-error-port)
            "Abstract Unix-domain socket hole is OPEN, guix-daemon is VULNERABLE!~%"))
Share Button

Source: Planet GNU