summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorReepca Russelstein <reepca@russelstein.xyz>2026-06-15 20:37:53 -0500
committerLudovic Courtès <ludo@gnu.org>2026-07-02 19:42:47 +0200
commit26d7eb8a4adafc648ef035e91b6bbc4945d4c962 (patch)
tree18461d366e7f1d9a6882817bac4a191e240fa318
parent3e5c3217f334805531e5db1680d97490999253f7 (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-xguix/scripts/substitute.scm61
-rw-r--r--tests/substitute.scm20
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
636delete 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.."
150URL: example.nar 166URL: example.nar
151Compression: none 167Compression: none
152NarHash: sha256:" (bytevector->nix-base32-string 168NarHash: sha256:" (bytevector->nix-base32-string
153 (sha256 (string->utf8 "Substitutable data."))) " 169 (plain-file-nar-sha256 "Substitutable data.")) "
154NarSize: 42 170NarSize: 42
155References: bar baz 171References: bar baz
156Deriver: " (%store-prefix) "/foo.drv 172Deriver: " (%store-prefix) "/foo.drv
@@ -819,7 +835,7 @@ Compression: lzip
819URL: example.nar 835URL: example.nar
820Compression: none 836Compression: none
821NarHash: sha256:" (bytevector->nix-base32-string 837NarHash: sha256:" (bytevector->nix-base32-string
822 (sha256 (string->utf8 "Substitutable data."))) " 838 (plain-file-nar-sha256 "Substitutable data.")) "
823NarSize: 42 839NarSize: 42
824References: bar baz 840References: bar baz
825Deriver: " (%store-prefix) "/foo.drv 841Deriver: " (%store-prefix) "/foo.drv