summaryrefslogtreecommitdiff
path: root/tests
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
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')
-rw-r--r--tests/nar.scm79
-rw-r--r--tests/publish.scm32
2 files changed, 101 insertions, 10 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
diff --git a/tests/publish.scm b/tests/publish.scm
index 3c80c50d515..91576abf139 100644
--- a/tests/publish.scm
+++ b/tests/publish.scm
@@ -96,6 +96,14 @@
96 (lambda () 96 (lambda ()
97 exp ...))))) 97 exp ...)))))
98 98
99;; 'call-with-temporary-output-file' produces a filename for a file that
100;; already exists, whereas this produces a filename for which no file exists
101;; inside a temporary directory.
102(define (call-with-temporary-output-filename proc)
103 (call-with-temporary-directory
104 (lambda (directory)
105 (proc (string-append directory "/output")))))
106
99;; guix-publish uses (current-processor-count) as the default number of 107;; guix-publish uses (current-processor-count) as the default number of
100;; workers, however on a system with a large number of cores, that large 108;; workers, however on a system with a large number of cores, that large
101;; number of worker threads being used in the course of these tests can end up 109;; number of worker threads being used in the course of these tests can end up
@@ -238,8 +246,8 @@ FileSize: ~a~%"
238 246
239(test-equal "/nar/*" 247(test-equal "/nar/*"
240 "bar" 248 "bar"
241 (call-with-temporary-output-file 249 (call-with-temporary-output-filename
242 (lambda (temp port) 250 (lambda (temp)
243 (let ((nar (utf8->string 251 (let ((nar (utf8->string
244 (http-get-body 252 (http-get-body
245 (publish-uri 253 (publish-uri
@@ -249,8 +257,8 @@ FileSize: ~a~%"
249 257
250(test-equal "/nar/gzip/*" 258(test-equal "/nar/gzip/*"
251 "bar" 259 "bar"
252 (call-with-temporary-output-file 260 (call-with-temporary-output-filename
253 (lambda (temp port) 261 (lambda (temp)
254 (let ((nar (http-get-port 262 (let ((nar (http-get-port
255 (publish-uri 263 (publish-uri
256 (string-append "/nar/gzip/" (basename %item)))))) 264 (string-append "/nar/gzip/" (basename %item))))))
@@ -270,8 +278,8 @@ FileSize: ~a~%"
270 278
271(test-equal "/nar/lzip/*" 279(test-equal "/nar/lzip/*"
272 "bar" 280 "bar"
273 (call-with-temporary-output-file 281 (call-with-temporary-output-filename
274 (lambda (temp port) 282 (lambda (temp)
275 (let ((nar (http-get-port 283 (let ((nar (http-get-port
276 (publish-uri 284 (publish-uri
277 (string-append "/nar/lzip/" (basename %item)))))) 285 (string-append "/nar/lzip/" (basename %item))))))
@@ -282,8 +290,8 @@ FileSize: ~a~%"
282(unless (zstd-supported?) (test-skip 1)) 290(unless (zstd-supported?) (test-skip 1))
283(test-equal "/nar/zstd/*" 291(test-equal "/nar/zstd/*"
284 "bar" 292 "bar"
285 (call-with-temporary-output-file 293 (call-with-temporary-output-filename
286 (lambda (temp port) 294 (lambda (temp)
287 (let ((nar (http-get-port 295 (let ((nar (http-get-port
288 (publish-uri 296 (publish-uri
289 (string-append "/nar/zstd/" (basename %item)))))) 297 (string-append "/nar/zstd/" (basename %item))))))
@@ -408,8 +416,8 @@ FileSize: ~a~%"
408(test-equal "/nar/ with properly encoded '+' sign" 416(test-equal "/nar/ with properly encoded '+' sign"
409 "Congrats!" 417 "Congrats!"
410 (let ((item (add-text-to-store %store "fake-gtk+" "Congrats!"))) 418 (let ((item (add-text-to-store %store "fake-gtk+" "Congrats!")))
411 (call-with-temporary-output-file 419 (call-with-temporary-output-filename
412 (lambda (temp port) 420 (lambda (temp)
413 (let ((nar (utf8->string 421 (let ((nar (utf8->string
414 (http-get-body 422 (http-get-body
415 (publish-uri 423 (publish-uri
@@ -798,3 +806,7 @@ FileSize: ~a~%"
798 (http-post (publish-uri path)))))) 806 (http-post (publish-uri path))))))
799 807
800(test-end "publish") 808(test-end "publish")
809
810;;; Local Variables:
811;;; eval: (put 'call-with-temporary-output-filename 'scheme-indent-function 0)
812;;; End: