diff options
| author | Reepca Russelstein <reepca@russelstein.xyz> | 2026-06-08 00:21:43 -0500 |
|---|---|---|
| committer | Ludovic Courtès <ludo@gnu.org> | 2026-07-02 19:42:47 +0200 |
| commit | ed0a9721f8a20d6ddcf6a0495302f502b3f7bb17 (patch) | |
| tree | ba06909769b65ca8dbf8b40fc87cdf6fe12ff5ce /tests/nar.scm | |
| parent | 980d0d6f76ce6c62ce127245e5396044672cf172 (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.scm | 79 |
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 | ||
