summaryrefslogtreecommitdiff
path: root/guix/scripts/perform-download.scm
blob: f74aa83f0d47ff4049d469615fb9e32dff6488a9 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
;;; GNU Guix --- Functional package management for GNU
;;; Copyright © 2016-2018, 2020, 2023-2024 Ludovic Courtès <ludo@gnu.org>
;;;
;;; This file is part of GNU Guix.
;;;
;;; GNU Guix is free software; you can redistribute it and/or modify it
;;; under the terms of the GNU General Public License as published by
;;; the Free Software Foundation; either version 3 of the License, or (at
;;; your option) any later version.
;;;
;;; GNU Guix is distributed in the hope that it will be useful, but
;;; WITHOUT ANY WARRANTY; without even the implied warranty of
;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
;;; GNU General Public License for more details.
;;;
;;; You should have received a copy of the GNU General Public License
;;; along with GNU Guix.  If not, see <http://www.gnu.org/licenses/>.

(define-module (guix scripts perform-download)
  #:use-module (guix ui)
  #:use-module (guix scripts)
  #:use-module (guix derivations)
  #:use-module ((guix store) #:select (derivation-path? store-path?))
  #:use-module ((guix build utils) #:select (store-file-name?
                                             strip-store-file-name))
  #:autoload   (guix build download) (%download-methods url-fetch)
  #:autoload   (guix build git) (git-fetch-with-fallback)
  #:autoload   (guix config) (%git)
  #:use-module (ice-9 match)
  #:use-module (ice-9 sandbox)
  #:export (guix-perform-download
            ;; exported so that eval-in-sandbox can find this
            syntax-noop))

;; This program is a helper for the daemon's 'download' built-in builder.

(define-syntax derivation-let
  (syntax-rules ()
    ((_ drv ((id name) rest ...) body ...)
     (let ((id (assoc-ref (derivation-builder-environment-vars drv)
                          name)))
       (derivation-let drv (rest ...) body ...)))
    ((_ drv () body ...)
     (begin body ...))))

(define-syntax-rule (syntax-noop ...) #t)

;; Bindings to be made available in the sandbox in which mirror procedures are
;; evaluated.  We opt for a somewhat conservative selection.
(define %safe-bindings
  `( ;; Historically used, must be available for backwards compatibility
    ((guile)
     lambda begin define string-append symbol->string list quote
     (noop . module-autoload!)
     (noop . current-module))
    ((guix base16) bytevector->base16-string base16-string->bytevector)
    ((guix base32)
     bytevector->base32-string bytevector->nix-base32-string
     base32-string->bytevector nix-base32-string->bytevector)
    ((guix scripts perform-download)
     (syntax-noop . use-modules))
    ;; Potentially useful for custom content-addressed-mirrors and future
    ;; changes
    ((guile) symbol?)
    ((rnrs bytevectors)
     bytevector? bytevector=? bytevector-length bytevector-u8-ref
     bytevector->u8-list u8-list->bytevector utf8->string)
    ,@core-bindings
    ,@string-bindings
    ,@list-bindings
    ,@pair-bindings
    ,@alist-bindings
    ,@iteration-bindings
    ,@number-bindings
    ,@predicate-bindings))

(define %sandbox-module
  (make-sandbox-module %safe-bindings))

(define* (read/safe #:optional (port (current-input-port)))
  (with-fluids ((read-eval? #f))
    (parameterize ((read-hash-procedures '()))
      (read port))))

(define (eval-content-addressed-mirrors content-addressed-mirrors file algo hash)
  "Evaluate the expression CONTENT-ADDRESSED-MIRRORS in a sandbox, and produce
a list of wrapper procedures for safely calling the list of procedures that
CONTENT-ADDRESSED-MIRRORS evaluates to."
  (map const
       (eval-in-sandbox `(map (lambda (proc)
                                (proc ,file ',algo ,hash))
                              (let ()
                                ,content-addressed-mirrors))
                        #:bindings %safe-bindings
                        #:module %sandbox-module)))

(define (assert-store-file file)
  "Canonicalize FILE and exit if the result is not in the store.  Return the
result of canonicalization."
  (let ((canon (canonicalize-path file)))
    (unless (store-file-name? canon)
      (leave (G_ "~S is not in the store~%") canon))
    canon))

(define (call-with-input-file/no-symlinks file proc)
  (call-with-port (open file (logior O_NOFOLLOW O_RDONLY))
    proc))

(define* (perform-download drv output
                           #:key print-build-trace?)
  "Perform the download described by DRV, a fixed-output derivation, to
OUTPUT.

Note: OUTPUT may differ from the 'out' value of DRV, notably for 'bmCheck' or
'bmRepair' builds."
  (derivation-let drv ((url "url")
                       (executable "executable")
                       (mirrors "mirrors")
                       (content-addressed-mirrors "content-addressed-mirrors")
                       (disarchive-mirrors "disarchive-mirrors")
                       (download-methods "download-methods"))
    (unless url
      (leave (G_ "~a: missing URL~%") (derivation-file-name drv)))

    (let* ((mirrors
            (and=> mirrors assert-store-file))
           (content-addressed-mirrors
            (and=> content-addressed-mirrors assert-store-file))
           (disarchive-mirrors
            (and=> disarchive-mirrors assert-store-file))
           (url        (call-with-input-string url read/safe))
           (drv-output (assoc-ref (derivation-outputs drv) "out"))
           (algo       (derivation-output-hash-algo drv-output))
           (hash       (derivation-output-hash drv-output)))
      ;; We're invoked by the daemon, which gives us write access to OUTPUT.
      (when (parameterize ((%download-methods
                            (and download-methods
                                 (call-with-input-string download-methods
                                   read/safe))))
              (url-fetch url output
                         #:print-build-trace? print-build-trace?
                         #:mirrors (if mirrors
                                       (call-with-input-file/no-symlinks
                                           mirrors
                                         read/safe)
                                       '())
                         #:content-addressed-mirrors
                         (if content-addressed-mirrors
                             (call-with-input-file/no-symlinks
                                 content-addressed-mirrors
                               (lambda (port)
                                 (eval-content-addressed-mirrors
                                  (read/safe port)
                                  (strip-store-file-name output)
                                  algo
                                  hash)))
                             '())
                         #:disarchive-mirrors
                         (if disarchive-mirrors
                             (call-with-input-file/no-symlinks
                                 disarchive-mirrors
                               read/safe)
                             '())
                         #:hashes `((,algo . ,hash))

                         ;; Since DRV's output hash is known, X.509 certificate
                         ;; validation is pointless.
                         #:verify-certificate? #f))
        (when (and executable (string=? executable "1"))
          (chmod output #o755))))))

(define* (perform-git-download drv output
                               #:key print-build-trace?)
  "Perform the download described by DRV, a fixed-output derivation, to
OUTPUT.

Note: OUTPUT may differ from the 'out' value of DRV, notably for 'bmCheck' or
'bmRepair' builds."
  (derivation-let drv ((url "url")
                       (commit "commit")
                       (recursive? "recursive?")
                       (download-methods "download-methods"))
    (unless url
      (leave (G_ "~a: missing Git URL~%") (derivation-file-name drv)))
    (unless commit
      (leave (G_ "~a: missing Git commit~%") (derivation-file-name drv)))

    (let* ((url        (call-with-input-string url read/safe))
           (recursive? (and recursive?
                            (call-with-input-string recursive? read/safe)))
           (drv-output (assoc-ref (derivation-outputs drv) "out"))
           (algo       (derivation-output-hash-algo drv-output))
           (hash       (derivation-output-hash drv-output)))
      ;; Commands such as 'git submodule' expect Coreutils and sed (among
      ;; others) to be in $PATH.  The 'git' package in Guix should address it
      ;; with wrappers but packages on other distros such as Debian may rely
      ;; on ambient authority, hence the PATH value below.
      (setenv "PATH" "/run/current-system/profile/bin:/bin:/usr/bin")

      (parameterize ((%download-methods
                      (and download-methods
                           (call-with-input-string download-methods
                             read/safe))))
        ;; Note: When doing a '--check' build, DRV-OUTPUT and OUTPUT are
        ;; different, hence the #:item argument below.
        (git-fetch-with-fallback url commit output
                                 #:hash hash
                                 #:hash-algorithm algo
                                 #:recursive? recursive?
                                 #:item (derivation-output-path drv-output)
                                 #:git-command %git)))))

(define (assert-low-privileges)
  (when (zero? (getuid))
    (leave (G_ "refusing to run with elevated privileges (UID ~a)~%")
           (getuid))))

(define-command (guix-perform-download . args)
  (category internal)
  (synopsis "perform download described by fixed-output derivations")

  ;; This is an "out-of-band" download in that this code is executed directly
  ;; by the daemon and not explicitly described as an input of the derivation.
  ;; This allows us to sidestep bootstrapping problems, such as downloading
  ;; the source code of GnuTLS over HTTPS before we have built GnuTLS.  See
  ;; <https://bugs.gnu.org/22774>.

  (define print-build-trace?
    (match (getenv "_NIX_OPTIONS")
      (#f #f)
      (str (string-contains str "print-extended-build-trace=1"))))

  ;; We read untrusted input, best to be sure this is #f!
  (fluid-set! read-eval? #f)
  ;; ... and out of an abundance of caution, remove the ability to use '#.'
  ;; constructs entirely
  (read-hash-procedures '())

  ;; This program must be invoked by guix-daemon under an unprivileged UID to
  ;; prevent things downloading from 'file:///etc/shadow'.  (That means we
  ;; exclude users who did not pass '--build-users-group'.)
  (with-error-handling
    (match args
      (((? derivation-path? drv) (? store-path? output))
       (assert-low-privileges)
       (let* ((drv (read-derivation-from-file drv))
              (download (match (derivation-builder drv)
                          ("builtin:download" perform-download)
                          ("builtin:git-download" perform-git-download)
                          (unknown (leave (G_ "~a: unknown builtin builder")
                                          unknown))))
              (drv-output (assoc-ref (derivation-outputs drv) "out"))
              (algo       (derivation-output-hash-algo drv-output))
              (hash       (derivation-output-hash drv-output)))
         (unless (and hash algo)
           (leave (G_ "~a is not a fixed-output derivation~%")
                  (derivation-file-name drv)))

         (download drv output #:print-build-trace? print-build-trace?)))
      (("--version")
       (show-version-and-exit))
      (x
       (leave
        (G_ "fixed-output derivation and output file name expected~%"))))))

;; Local Variables:
;; eval: (put 'derivation-let 'scheme-indent-function 2)
;; End:

;; perform-download.scm ends here