summaryrefslogtreecommitdiff
path: root/tests/nar.scm
diff options
context:
space:
mode:
authorReepca Russelstein <reepca@russelstein.xyz>2026-06-08 00:21:43 -0500
committerLudovic Courtès <ludo@gnu.org>2026-07-02 19:42:47 +0200
commited0a9721f8a20d6ddcf6a0495302f502b3f7bb17 (patch)
treeba06909769b65ca8dbf8b40fc87cdf6fe12ff5ce /tests/nar.scm
parent980d0d6f76ce6c62ce127245e5396044672cf172 (diff)
guix: serialization: validate directory entry names in fold-archive [security fix].
Also validate that they are in strictly ascending order, which also ensures that there are no duplicate names. When 'guix substitute' fetches a nar, it does so with a validly-signed hash already known from the narinfo. But it can't verify that the hash of the nar it's currently fetching matches until the full nar is downloaded. Until then, 'download-nar' will extract the nar into the specified destination using 'restore-path', which avoids having to keep a file of unbounded size in memory. Critically, this means that the input that 'restore-path' (and by extension 'fold-archive') is processing is untrusted, since substitute server TLS certificates aren't verified (the narinfo signatures are supposed to make it unnecessary). As such, the scope of harm that can be caused by a malicious nar being processed by 'restore-file' needs to be minimized. * guix/serialization.scm (valid-nar-file-name?): new procedure. (fold-archive): Use it to verify that directory entry names do not contain '/' or '\0', and they are not equal to ".", "..", or "". Also verify that they are in strictly ascending order. (call-with-port*): new procedure. (dump-file): use O_EXCL and O_NOFOLLOW. This precaution ensures that even if 'restore-file' is somehow tricked into writing to a symlink (not currently believed to be possible), it will result in an error. * guix/store/deduplication.scm (call-with-fresh-output-file): new procedure. (dump-file/deduplicate): use it for similar reasons as 'dump-file'. * tests/nar.scm (call-with-tree-port, port-bad-nar?): new procedures. ("write-file-tree + fold-archive, unsorted directory entries", "write-file-tree + fold-archive, duplicate directory entries", "write-file-tree + fold-archive, invalid directory entries"): new tests. * tests/publish.scm (call-with-temporary-output-filename): new procedure. ("/nar/*", "/nar/gzip/*", "/nar/lzip/*", "/nar/zstd/*", "/nar/ with properly encoded '+' sign"): use it in these test cases so that the output filename doesn't name an already-existing file. Change-Id: I41f248c13d7af787233afad5cae102056329a68b Signed-off-by: Ludovic Courtès <ludo@gnu.org>
Diffstat (limited to 'tests/nar.scm')
-rw-r--r--tests/nar.scm79
1 files changed, 79 insertions, 0 deletions
diff --git a/tests/nar.scm b/tests/nar.scm
index 98752f20884..b446ea3d923 100644
--- a/tests/nar.scm
+++ b/tests/nar.scm
@@ -35,6 +35,7 @@
35 #:use-module (srfi srfi-34) 35 #:use-module (srfi srfi-34)
36 #:use-module (srfi srfi-35) 36 #:use-module (srfi srfi-35)
37 #:use-module (srfi srfi-64) 37 #:use-module (srfi srfi-64)
38 #:use-module (ice-9 binary-ports)
38 #:use-module (ice-9 ftw) 39 #:use-module (ice-9 ftw)
39 #:use-module (ice-9 regex) 40 #:use-module (ice-9 regex)
40 #:use-module ((ice-9 control) #:select (let/ec)) 41 #:use-module ((ice-9 control) #:select (let/ec))
@@ -268,6 +269,84 @@
268 (open-bytevector-input-port (get-bytevector)) 269 (open-bytevector-input-port (get-bytevector))
269 "R")))) 270 "R"))))
270 271
272(define (call-with-tree-port tree proc)
273 (let ((bv (call-with-output-bytevector
274 (lambda (port)
275 (apply write-file-tree "root" port tree)))))
276 (call-with-input-bytevector bv proc)))
277
278(define (port-bad-nar? port)
279 (guard (c ((nar-error? c)
280 (pk 'nar-error c)
281 #t))
282 (dynamic-wind
283 (const #t)
284 (lambda ()
285 (rm-rf %test-dir)
286 (mkdir %test-dir)
287 (restore-file port (string-append %test-dir "/foo")))
288 (lambda ()
289 (false-if-exception (rm-rf %test-dir))))))
290
291(test-assert "write-file-tree + fold-archive, unsorted directory entries"
292 (let* ((unsorted-tree (list #:file-type+size
293 (match-lambda
294 ("root" (values 'directory 0))
295 ("root/c" (values 'regular 1))
296 ("root/b" (values 'regular 1))
297 ("root/a" (values 'regular 1)))
298 #:file-port
299 (match-lambda
300 ("root/c" (open-input-string "c"))
301 ("root/b" (open-input-string "b"))
302 ("root/a" (open-input-string "a")))
303 #:directory-entries
304 (match-lambda
305 ("root" '("c" "b" "a")))
306 ;; We wish to deliberately create invalid
307 ;; entries
308 #:postprocess-entries identity)))
309 (call-with-tree-port unsorted-tree port-bad-nar?)))
310
311(test-assert "write-file-tree + fold-archive, duplicate directory entries"
312 (let* ((duplicates-tree (list #:file-type+size
313 (match-lambda
314 ("root" (values 'directory 0))
315 ("root/a" (values 'regular 1)))
316 #:file-port
317 (match-lambda
318 ("root/a" (open-input-string "a")))
319 #:directory-entries
320 (match-lambda
321 ("root" '("a" "a" "a")))
322 ;; We wish to deliberately create invalid
323 ;; entries
324 #:postprocess-entries identity)))
325 (call-with-tree-port duplicates-tree port-bad-nar?)))
326
327(test-assert "write-file-tree + fold-archive, invalid directory entries"
328 (let* ((invalid-names (list "." ".." "" "../" "./"
329 "/" "/bin/shh" "../../etc/passwwwd"
330 (string #\nul) (string #\. #\/ #\nul)))
331 (invalid-name-trees (map (lambda (name)
332 (list #:file-type+size
333 (match-lambda
334 ("root" (values 'directory 0))
335 (_ (values 'regular 1)))
336 #:file-port
337 (lambda (_)
338 (open-input-string "a"))
339 #:directory-entries
340 (match-lambda
341 ("root" (list name)))
342 ;; We wish to deliberately create
343 ;; invalid entries
344 #:postprocess-entries identity))
345 invalid-names)))
346 (every (lambda (tree)
347 (call-with-tree-port tree port-bad-nar?))
348 invalid-name-trees)))
349
271(test-equal "write-file-tree + fold-archive, flat file" 350(test-equal "write-file-tree + fold-archive, flat file"
272 '(("R" regular "abcdefg")) 351 '(("R" regular "abcdefg"))
273 352