diff options
| author | Reepca Russelstein <reepca@russelstein.xyz> | 2026-06-15 20:37:53 -0500 |
|---|---|---|
| committer | Ludovic Courtès <ludo@gnu.org> | 2026-07-02 19:42:47 +0200 |
| commit | 26d7eb8a4adafc648ef035e91b6bbc4945d4c962 (patch) | |
| tree | 18461d366e7f1d9a6882817bac4a191e240fa318 | |
| parent | 3e5c3217f334805531e5db1680d97490999253f7 (diff) | |
scripts: substitute: restored nars only appear after verification.
The hash of a nar is only known once 'download-nar' completes. By that point
the nar may already have been partially-restored to its destination for some
time. Additionally, after a hash mismatch is detected, 'guix substitute'
leaves the invalid contents at the specified destination. This means that
untrusted, attacker-controlled contents are present at what at some point may
have been or at some point may become a valid store path. It is entirely
possible that a user or program won't check that a given store item is valid
before trying to use it (it may be that 'guix gc' was run while an obscured
reference was held). Let's protect them in that case by ensuring that
unverified contents are kept elsewhere, in a temporary directory.
While implementing this one such program was discovered: tests/substitute.scm.
It actually incorrectly computes the hashes of the nars it supplies to (guix
scripts substitute), computing the hash of the plain contents instead of the
hash of the nar. These tests nevertheless passed because they didn't check
what (guix scripts substitute) gave as output, instead only checking that the
expected contents were at the specified destination.
* guix/scripts/substitute.scm (call-with-temporary-directory-in): new
procedure.
(guix-substitute): use it to create a temporary directory next to the final
destination to restore the nar inside of. Once it is fully restored, if the
hash matches, move it to the final destination, deleting the file currently
there if it already exists.
* tests/substitute.scm (plain-file-nar-sha256): new procedure.
(%narinfo, "substitute, narinfo with several URLS" test): use it.
Change-Id: Ifeb9b4c912f9d9b2e7477d237da04e7d5175ff83
Signed-off-by: Ludovic Courtès <ludo@gnu.org>
| -rwxr-xr-x | guix/scripts/substitute.scm | 61 | ||||
| -rw-r--r-- | tests/substitute.scm | 20 |
2 files changed, 69 insertions, 12 deletions
diff --git a/guix/scripts/substitute.scm b/guix/scripts/substitute.scm index 49f5389fe27..383661511e2 100755 --- a/guix/scripts/substitute.scm +++ b/guix/scripts/substitute.scm | |||
| @@ -44,7 +44,7 @@ | |||
| 44 | . guix:open-connection-for-uri))) | 44 | . guix:open-connection-for-uri))) |
| 45 | #:use-module (guix progress) | 45 | #:use-module (guix progress) |
| 46 | #:use-module ((guix build syscalls) | 46 | #:use-module ((guix build syscalls) |
| 47 | #:select (set-thread-name)) | 47 | #:select (set-thread-name mkdtemp!)) |
| 48 | #:use-module (ice-9 rdelim) | 48 | #:use-module (ice-9 rdelim) |
| 49 | #:use-module (ice-9 match) | 49 | #:use-module (ice-9 match) |
| 50 | #:use-module (ice-9 format) | 50 | #:use-module (ice-9 format) |
| @@ -630,6 +630,26 @@ default value." | |||
| 630 | ;; use the current output port instead. | 630 | ;; use the current output port instead. |
| 631 | (make-parameter 4)) | 631 | (make-parameter 4)) |
| 632 | 632 | ||
| 633 | ;; XXX: copied from (guix utils) | ||
| 634 | (define (call-with-temporary-directory-in directory proc) | ||
| 635 | "Call PROC with a name of a temporary directory; close the directory and | ||
| 636 | delete it when leaving the dynamic extent of this call." | ||
| 637 | (let* ((template (string-append directory "/guix-directory-" | ||
| 638 | ;; In case the temporary directory may be | ||
| 639 | ;; garbage collected, including our pid | ||
| 640 | ;; ensures that no live process can mistake | ||
| 641 | ;; our freshly-created instance for their | ||
| 642 | ;; old deleted one. | ||
| 643 | (number->string (getpid)) | ||
| 644 | ".XXXXXX")) | ||
| 645 | (tmp-dir (mkdtemp! template))) | ||
| 646 | (dynamic-wind | ||
| 647 | (const #t) | ||
| 648 | (lambda () | ||
| 649 | (proc tmp-dir)) | ||
| 650 | (lambda () | ||
| 651 | (false-if-exception (delete-file-recursively tmp-dir)))))) | ||
| 652 | |||
| 633 | (define-command (guix-substitute . args) | 653 | (define-command (guix-substitute . args) |
| 634 | (category internal) | 654 | (category internal) |
| 635 | (synopsis "implement the build daemon's substituter protocol") | 655 | (synopsis "implement the build daemon's substituter protocol") |
| @@ -712,15 +732,35 @@ default value." | |||
| 712 | actual-hash | 732 | actual-hash |
| 713 | cpu-usage | 733 | cpu-usage |
| 714 | (with-cpu-usage-monitoring | 734 | (with-cpu-usage-monitoring |
| 715 | (process-substitution | 735 | ;; Restore inside a temporary directory until the hash |
| 716 | store-path destination | 736 | ;; can be verified so that no dangling references a |
| 717 | #:cache-urls (substitute-urls) | 737 | ;; user may have laying around will point to |
| 718 | #:acl (current-acl) | 738 | ;; attacker-controlled content in the meantime. |
| 719 | #:deduplicate? deduplicate? | 739 | (call-with-temporary-directory-in (dirname destination) |
| 720 | #:print-build-trace? | 740 | (lambda (temp-directory) |
| 721 | print-build-trace? | 741 | (let* ((temp-destination |
| 722 | #:fast-decompression? | 742 | (string-append temp-directory "/restored")) |
| 723 | fast-decompression?)))) | 743 | (narinfo |
| 744 | expected-hash | ||
| 745 | actual-hash | ||
| 746 | (process-substitution | ||
| 747 | store-path temp-destination | ||
| 748 | #:cache-urls (substitute-urls) | ||
| 749 | #:acl (current-acl) | ||
| 750 | #:deduplicate? deduplicate? | ||
| 751 | #:print-build-trace? | ||
| 752 | print-build-trace? | ||
| 753 | #:fast-decompression? | ||
| 754 | fast-decompression?))) | ||
| 755 | (when (and expected-hash actual-hash | ||
| 756 | (bytevector=? actual-hash | ||
| 757 | expected-hash)) | ||
| 758 | (catch 'system-error | ||
| 759 | (lambda () | ||
| 760 | (delete-file-recursively destination)) | ||
| 761 | (const #f)) | ||
| 762 | (rename-file temp-destination destination)) | ||
| 763 | (values narinfo expected-hash actual-hash))))))) | ||
| 724 | 764 | ||
| 725 | (if expected-hash | 765 | (if expected-hash |
| 726 | (begin | 766 | (begin |
| @@ -777,6 +817,7 @@ default value." | |||
| 777 | 817 | ||
| 778 | ;;; Local Variables: | 818 | ;;; Local Variables: |
| 779 | ;;; eval: (put 'with-redirected-error-port 'scheme-indent-function 0) | 819 | ;;; eval: (put 'with-redirected-error-port 'scheme-indent-function 0) |
| 820 | ;;; eval: (put 'call-with-temporary-directory-in 'scheme-indent-function 1) | ||
| 780 | ;;; End: | 821 | ;;; End: |
| 781 | 822 | ||
| 782 | ;;; substitute.scm ends here | 823 | ;;; substitute.scm ends here |
diff --git a/tests/substitute.scm b/tests/substitute.scm index 10b74db084e..086bd588358 100644 --- a/tests/substitute.scm +++ b/tests/substitute.scm | |||
| @@ -39,6 +39,7 @@ | |||
| 39 | #:use-module (rnrs io ports) | 39 | #:use-module (rnrs io ports) |
| 40 | #:use-module (web uri) | 40 | #:use-module (web uri) |
| 41 | #:use-module (ice-9 regex) | 41 | #:use-module (ice-9 regex) |
| 42 | #:use-module (ice-9 binary-ports) | ||
| 42 | #:use-module (srfi srfi-11) | 43 | #:use-module (srfi srfi-11) |
| 43 | #:use-module (srfi srfi-26) | 44 | #:use-module (srfi srfi-26) |
| 44 | #:use-module (srfi srfi-34) | 45 | #:use-module (srfi srfi-34) |
| @@ -142,6 +143,21 @@ version identifier.." | |||
| 142 | ;; <https://www.rfc-editor.org/rfc/rfc5737>. | 143 | ;; <https://www.rfc-editor.org/rfc/rfc5737>. |
| 143 | "http://203.0.113.1") | 144 | "http://203.0.113.1") |
| 144 | 145 | ||
| 146 | (define (plain-file-nar-sha256 string) | ||
| 147 | (sha256 (call-with-output-bytevector | ||
| 148 | (lambda (port) | ||
| 149 | (let ((bv (string->utf8 string))) | ||
| 150 | (call-with-input-bytevector | ||
| 151 | bv | ||
| 152 | (lambda (contents) | ||
| 153 | (write-file-tree #t port | ||
| 154 | #:file-type+size | ||
| 155 | (lambda (_) | ||
| 156 | (values 'regular | ||
| 157 | (bytevector-length | ||
| 158 | bv))) | ||
| 159 | #:file-port | ||
| 160 | (const contents))))))))) | ||
| 145 | 161 | ||
| 146 | (define %narinfo | 162 | (define %narinfo |
| 147 | ;; Skeleton of the narinfo used below. | 163 | ;; Skeleton of the narinfo used below. |
| @@ -150,7 +166,7 @@ version identifier.." | |||
| 150 | URL: example.nar | 166 | URL: example.nar |
| 151 | Compression: none | 167 | Compression: none |
| 152 | NarHash: sha256:" (bytevector->nix-base32-string | 168 | NarHash: sha256:" (bytevector->nix-base32-string |
| 153 | (sha256 (string->utf8 "Substitutable data."))) " | 169 | (plain-file-nar-sha256 "Substitutable data.")) " |
| 154 | NarSize: 42 | 170 | NarSize: 42 |
| 155 | References: bar baz | 171 | References: bar baz |
| 156 | Deriver: " (%store-prefix) "/foo.drv | 172 | Deriver: " (%store-prefix) "/foo.drv |
| @@ -819,7 +835,7 @@ Compression: lzip | |||
| 819 | URL: example.nar | 835 | URL: example.nar |
| 820 | Compression: none | 836 | Compression: none |
| 821 | NarHash: sha256:" (bytevector->nix-base32-string | 837 | NarHash: sha256:" (bytevector->nix-base32-string |
| 822 | (sha256 (string->utf8 "Substitutable data."))) " | 838 | (plain-file-nar-sha256 "Substitutable data.")) " |
| 823 | NarSize: 42 | 839 | NarSize: 42 |
| 824 | References: bar baz | 840 | References: bar baz |
| 825 | Deriver: " (%store-prefix) "/foo.drv | 841 | Deriver: " (%store-prefix) "/foo.drv |
