A security issue has been identified in
guix-daemon
which allows 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. You
are strongly advised to upgrade your daemon now (see instructions
below), especially on multi-user systems.
This exploit requires the ability to start a derivation build and the ability to
run arbitrary code with access to the store in the root PID namespace on the
machine the build occurs on. As such, this represents an increased risk
primarily to multi-user systems and systems using dedicated privilege-separation
users for various daemons: without special sandboxing measures, any process of
theirs can take advantage of this vulnerability.
Vulnerability
For a very long time, guix-daemon
has helpfully made the outputs of failed
derivation builds
available
at the same location they were at in the build container. This has aided greatly
especially in situations where test suites require the package to already be
installed in order to run, as it allows one to re-run the test suite
interactively outside of the container when built with --keep-failed
. This
transferral of store items from inside the chroot to the real store was
implemented with a simple rename
, and no modification of the store item or
any files it may contain.
If an attacker starts a build of a derivation that creates a binary with the
setuid and/or setgid bit in an output directory, then, and the build fails, that
binary will be accessible unaltered for anybody on the system. The attacker or a
cooperating user can then execute the binary, gain the privileges, and from
there use a combination of signals and procfs to freeze a builder, open any file
it has open via /proc/$PID/fd
, and overwrite it with whatever it wants. This
manipulation of builds can happen regardless of which user started the build, so
it can work not only for producing compromised outputs for commonly-used
programs before anybody else uses them, but also for compromising any builds
another user happens to start.
A related vulnerability was also discovered concerning the outputs of
successful builds. These were
moved -
also via rename()
- outside of the container prior to having their
permissions, ownership, and timestamps
canonicalized. This
means that there also exists a window of time for a successful build's outputs
during which a setuid/setgid binary can be executed.
In general, any time that a build user running a build for some submitter can
get a setuid/setgid binary to a place the submitter can execute it, it is
possible for the submitter to use it to take over the build user. This situation
always occurs when --disable-chroot
is passed to guix-daemon
. This holds
even in the case where there are no dedicated build users, and builds happen
under the same user the daemon runs as, as happens during make check
in the
guix repository. Consequently, if a permissive umask that allows execute
permission for untrusted users on directories all the way to a user's guix
checkout is used, an attacker can use that user's test-environment daemon to
gain control over their user while make check
is running.
Mitigation
This security issue has been fixed by
two
commits. Users
should make sure they have updated to the second commit to be protected from
this vulnerability. Upgrade instructions are in the following section. If there
is a possibility that a failed build has left a setuid/setgid binary lying
around in the store by accident, run guix gc
to remove all failed build
outputs.
The fix was accomplished by sanitizing the permissions of all files in a failed
build output prior to moving it to the store, and also by waiting to move
successful build outputs to the store until after their permissions had been
canonicalized. The sanitizing was done in such a way as to preserve as many
non-security-critical properties of failed build outputs as possible to aid in
debugging. After applying these two commits, the guix
package in Guix was
updated
so that guix-daemon
deployed using it would use the fixed version.
If you are using --disable-chroot
, whether with dedicated build users or not,
make sure that access to your daemon's socket is restricted to trusted
users. This particularly affects anyone running make check
and anyone running
on GNU/Hurd. The former should either manually remove execute permission for
untrusted users on their guix checkout or apply this
patch, which restricts access to the
test-environment daemon to the user running the tests. The latter should adjust
the ownership and permissions of /var/guix/daemon-socket
, which can be done
for Guix System users using the new socket-directory-{perms,group,user}
fields
in this patch.
A proof of concept is available at the end of this post. One can run this code
with:
guix repl -- setuid-exposure-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 your system is not vulnerable
, otherwise the last line will contain YOUR SYSTEM IS VULNERABLE
.
Upgrading
Due to the severity of this security advisory, we strongly recommend
all users to upgrade their 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 running as a package manager on other distributions, 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.
Conclusion
Even with the sandboxing features of modern kernels, it can be quite challenging
to synthesize a situation in which two users on the same system who are
determined to cooperate nevertheless cannot. Guix has an especially difficult
job because it needs to not only realize such a situation, but also maintain the
ability to interact with both users itself, while not allowing them to cooperate
through itself in unintended ways. Keeping failed build outputs around for
debugging introduced a vulnerability, but finding that vulnerability because of
it enabled the discovery of an additional vulnerability that would have existed
anyway, and prompted the use of mechanisms for securing access to the guix
daemon.
I would like to thank Ludovic Courtès for giving feedback on these
vulnerabilities and their fixes — discussion of which led to discovering the
vulnerable time window with successful build outputs — and also for helping me
to discover that my email server was broken.
Proof of Concept
Below is code to check if your guix-daemon
is vulnerable to this exploit. Save
this file as setuid-exposure-vuln-check.scm
and run following the instructions
above, in "Mitigation."
(use-modules (guix)
(srfi srfi-34))
(define maybe-setuid-file
;; Attempt to create a setuid file in the store, with one of the build
;; users as its owner.
(computed-file "maybe-setuid-file"
#~(begin
(call-with-output-file #$output (const #t))
(chmod #$output #o6000)
;; Failing causes guix-daemon to copy the output from
;; its temporary location back to the store.
(exit 1))))
(with-store store
(let* ((drv (run-with-store store
(lower-object maybe-setuid-file)))
(out (derivation->output-path drv)))
(guard (c (#t
(if (zero? (logand #o6000 (stat:perms (stat out))))
(format #t "~a is not setuid: your system is not \
vulnerable.~%"
out)
(format #t "~a is setuid: YOUR SYSTEM IS VULNERABLE.
Run 'guix gc' to remove that file and upgrade.~%"
out))))
(build-things store (list (derivation-file-name drv))))))