Add policy-driven jailed build config

This commit is contained in:
2026-04-09 14:32:18 +02:00
parent d2ff6fdaee
commit 94b6cd9841
3 changed files with 461 additions and 71 deletions
+353 -46
View File
@@ -228,11 +228,195 @@
(define (current-user-home)
(user-home-directory (current-user-name)))
(define jail-default-path
"/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin")
(define (native-build-executor-property-ref executor key default)
(match (assoc key (native-build-executor-properties executor))
((_ . value) value)
(#f default)))
(define (string-list? value)
(and (list? value)
(every string? value)))
(define (environment-bindings? value)
(and (list? value)
(every (lambda (entry)
(and (pair? entry)
(string? (car entry))
(string? (cdr entry))))
value)))
(define (jail-mount-pairs? value)
(and (list? value)
(every (lambda (entry)
(match entry
((source target)
(and (string? source)
(string? target)))
((source . target)
(and (string? source)
(string? target)))
(_ #f)))
value)))
(define %build-jail-unspecified
(list 'build-jail-unspecified))
(define (build-jail-unspecified? value)
(eq? value %build-jail-unspecified))
(define* (build-jail-config #:key (name %build-jail-unspecified)
(network? %build-jail-unspecified)
(read-only-mounts %build-jail-unspecified)
(writable-mounts %build-jail-unspecified)
(mounts %build-jail-unspecified)
(env %build-jail-unspecified)
(user %build-jail-unspecified)
(home %build-jail-unspecified)
(workdir %build-jail-unspecified)
(directories %build-jail-unspecified)
(metadata-file %build-jail-unspecified)
(keep-root? %build-jail-unspecified))
(unless (or (build-jail-unspecified? name) (not name) (string? name))
(error "build jail config name must be a string or #f" name))
(unless (or (build-jail-unspecified? network?) (boolean? network?))
(error "build jail config network? must be a boolean" network?))
(unless (or (build-jail-unspecified? read-only-mounts)
(jail-mount-pairs? read-only-mounts))
(error "build jail config read-only-mounts must be source/target pairs" read-only-mounts))
(unless (or (build-jail-unspecified? writable-mounts)
(jail-mount-pairs? writable-mounts))
(error "build jail config writable-mounts must be source/target pairs" writable-mounts))
(unless (or (build-jail-unspecified? env)
(environment-bindings? env))
(error "build jail config env must be an association list of strings" env))
(unless (or (build-jail-unspecified? user) (not user) (string? user))
(error "build jail config user must be a string or #f" user))
(unless (or (build-jail-unspecified? home) (not home) (string? home))
(error "build jail config home must be a string or #f" home))
(unless (or (build-jail-unspecified? workdir) (string? workdir))
(error "build jail config workdir must be a string" workdir))
(unless (or (build-jail-unspecified? directories)
(string-list? directories))
(error "build jail config directories must be a list of strings" directories))
(unless (or (build-jail-unspecified? metadata-file) (not metadata-file) (string? metadata-file))
(error "build jail config metadata-file must be a string or #f" metadata-file))
(unless (or (build-jail-unspecified? keep-root?) (boolean? keep-root?))
(error "build jail config keep-root? must be a boolean" keep-root?))
`((name . ,name)
(network? . ,network?)
(read-only-mounts . ,read-only-mounts)
(writable-mounts . ,writable-mounts)
(mounts . ,mounts)
(env . ,env)
(user . ,user)
(home . ,home)
(workdir . ,workdir)
(directories . ,directories)
(metadata-file . ,metadata-file)
(keep-root? . ,keep-root?)))
(define (build-jail-config-ref config key default)
(match (assoc key config)
((_ . value) value)
(#f default)))
(define (build-jail-config-value config key default)
(let ((value (build-jail-config-ref config key %build-jail-unspecified)))
(if (build-jail-unspecified? value)
default
value)))
(define (alist-replace entries key value same-key?)
(cons (cons key value)
(filter (lambda (entry)
(not (same-key? (car entry) key)))
entries)))
(define (merge-environment-bindings base overrides)
(fold (lambda (entry result)
(alist-replace result (car entry) (cdr entry) string=?))
base
overrides))
(define (default-jail-environment user home)
(let ((effective-user (or user "build"))
(effective-home (or home "/tmp")))
`(("HOME" . ,effective-home)
("LOGNAME" . ,effective-user)
("PATH" . ,jail-default-path)
("SHELL" . "/bin/sh")
("TMPDIR" . "/tmp")
("USER" . ,effective-user))))
(define (build-jail-base-config executor)
(let* ((user (or (native-build-executor-property-ref executor 'user #f)
(current-user-name)))
(home (or (native-build-executor-property-ref executor 'home #f)
(user-home-directory user)
"/tmp")))
(build-jail-config
#:network? (native-build-executor-property-ref executor 'network? #f)
#:read-only-mounts (or (native-build-executor-property-ref executor 'read-only-mounts #f)
'())
#:writable-mounts (or (native-build-executor-property-ref executor 'writable-mounts #f)
'())
#:env (merge-environment-bindings
(default-jail-environment user home)
(or (native-build-executor-property-ref executor 'env #f)
'()))
#:user user
#:home home
#:workdir (or (native-build-executor-property-ref executor 'workdir #f)
"/tmp")
#:directories (or (native-build-executor-property-ref executor 'directories #f)
'())
#:metadata-file (native-build-executor-property-ref executor 'metadata-file #f)
#:keep-root? (native-build-executor-property-ref executor 'keep-root? #f))))
(define (build-jail-config-merge base override)
(build-jail-config
#:name (build-jail-config-value override 'name
(build-jail-config-value base 'name #f))
#:network? (build-jail-config-value override 'network?
(build-jail-config-value base 'network? #f))
#:read-only-mounts (append (build-jail-config-value base 'read-only-mounts '())
(build-jail-config-value override 'read-only-mounts '()))
#:writable-mounts (append (build-jail-config-value base 'writable-mounts '())
(build-jail-config-value override 'writable-mounts '()))
#:mounts (append (build-jail-config-value base 'mounts '())
(build-jail-config-value override 'mounts '()))
#:env (merge-environment-bindings (build-jail-config-value base 'env '())
(build-jail-config-value override 'env '()))
#:user (build-jail-config-value override 'user
(build-jail-config-value base 'user #f))
#:home (build-jail-config-value override 'home
(build-jail-config-value base 'home #f))
#:workdir (build-jail-config-value override 'workdir
(build-jail-config-value base 'workdir "/tmp"))
#:directories (append (build-jail-config-value base 'directories '())
(build-jail-config-value override 'directories '()))
#:metadata-file (build-jail-config-value override 'metadata-file
(build-jail-config-value base 'metadata-file #f))
#:keep-root? (build-jail-config-value override 'keep-root?
(build-jail-config-value base 'keep-root? #f))))
(define (default-jail-build-executor)
(if (jail-build-available?)
(jail-native-build-executor #:host-name (or (safe-command-output "hostname") "localhost")
#:sudo-command "sudo -n")
(host-native-build-executor #:host-name (or (safe-command-output "hostname") "localhost"))))
(let* ((host-name (or (safe-command-output "hostname") "localhost"))
(user (current-user-name))
(home (or (current-user-home) "/tmp")))
(if (jail-build-available?)
(jail-native-build-executor #:host-name host-name
#:sudo-command "sudo -n"
#:network? #f
#:env (default-jail-environment user home)
#:user user
#:home home
#:workdir "/tmp"
#:directories '("/tmp"))
(host-native-build-executor #:host-name host-name))))
(define (default-copy-build-executor)
(default-jail-build-executor))
@@ -241,7 +425,8 @@
(default-jail-build-executor))
(define (jail-sudo-command executor)
(native-build-executor-ref executor 'sudo-command "sudo -n"))
(or (native-build-executor-property-ref executor 'sudo-command #f)
"sudo -n"))
(define (jail-nullfs-mount source target mode)
`(nullfs ,source ,target ,mode))
@@ -296,48 +481,157 @@
(shell-quote mountpoint)))
mountpoint))))
(define (jail-mount-pair-source entry)
(match entry
((source target) source)
((source . target) source)
(_ (error "invalid jail mount pair" entry))))
(define (jail-mount-pair-target entry)
(match entry
((source target) target)
((source . target) target)
(_ (error "invalid jail mount pair" entry))))
(define (build-jail-config-mounts config)
(append (map (lambda (entry)
(jail-nullfs-mount (jail-mount-pair-source entry)
(jail-mount-pair-target entry)
'ro))
(build-jail-config-value config 'read-only-mounts '()))
(map (lambda (entry)
(jail-nullfs-mount (jail-mount-pair-source entry)
(jail-mount-pair-target entry)
'rw))
(build-jail-config-value config 'writable-mounts '()))
(build-jail-config-value config 'mounts '())))
(define (build-jail-effective-directories config)
(delete-duplicates
(filter identity
(append (build-jail-config-value config 'directories '())
(list (build-jail-config-value config 'workdir "/tmp")
(build-jail-config-value config 'home #f))))
string=?))
(define (environment-variable-name-valid? name)
(and (string? name)
(> (string-length name) 0)
(let ((chars (string->list name)))
(and (let ((first (car chars)))
(or (char-alphabetic? first)
(char=? first #\_)))
(every (lambda (character)
(or (char-alphabetic? character)
(char-numeric? character)
(char=? character #\_)))
(cdr chars))))))
(define (environment-binding->shell-fragment entry)
(let ((name (car entry))
(value (cdr entry)))
(unless (environment-variable-name-valid? name)
(error "invalid jail environment variable name" name))
(string-append " "
name
"="
(shell-quote value))))
(define (build-jail-runner-script config)
(string-append
"#!/bin/sh\n"
"set -eu\n"
"cd " (shell-quote (build-jail-config-value config 'workdir "/tmp")) "\n"
"exec /usr/bin/env -i"
(string-concatenate
(map environment-binding->shell-fragment
(build-jail-config-value config 'env '())))
" /bin/sh /build.sh\n"))
(define (build-jail-command-string executor jail-root jail-name config runner-target)
(let ((jail-user (build-jail-config-value config 'user #f))
(network? (build-jail-config-value config 'network? #f)))
(string-append
(jail-sudo-command executor)
" jail -c path=" (shell-quote jail-root)
" name=" (shell-quote jail-name)
" host.hostname=" (shell-quote jail-name)
" exec.clean"
(if network?
""
" ip4=disable ip6=disable")
(if jail-user
(string-append " exec.jail_user=" (shell-quote jail-user))
"")
" command=/bin/sh " (shell-quote runner-target))))
(define (build-jail-run-metadata executor config jail-root jail-name script mount-specs)
`((executor . ,executor)
(name . ,jail-name)
(root . ,jail-root)
(network? . ,(build-jail-config-value config 'network? #f))
(user . ,(build-jail-config-value config 'user #f))
(home . ,(build-jail-config-value config 'home #f))
(workdir . ,(build-jail-config-value config 'workdir "/tmp"))
(env . ,(build-jail-config-value config 'env '()))
(mounts . ,mount-specs)
(directories . ,(build-jail-effective-directories config))
(script . ,script)
(command . ,(build-jail-command-string executor jail-root jail-name config "/run.sh"))))
(define* (run-script-in-temporary-jail executor script
#:key
name
config
(mounts '())
(directories '("/tmp")))
(let* ((jail-root (mktemp-directory "/tmp/fruix-build-jail.XXXXXX"))
(let* ((effective-config (build-jail-config-merge
(build-jail-config-merge
(build-jail-base-config executor)
(build-jail-config #:name name
#:mounts mounts
#:directories directories))
(or config
(build-jail-config))))
(mount-specs (build-jail-config-mounts effective-config))
(jail-root (mktemp-directory "/tmp/fruix-build-jail.XXXXXX"))
(script-path (string-append jail-root "/build.sh"))
(jail-name (string-append (or name "fruix-build")
(runner-path (string-append jail-root "/run.sh"))
(jail-name (string-append (or (build-jail-config-value effective-config 'name #f)
"fruix-build")
"-"
(store-hash-string (or name script) #:kind 'jail)))
(jail-user (current-user-name))
(jail-home (current-user-home))
(store-hash-string (or (build-jail-config-value effective-config 'name #f)
script)
#:kind 'jail)))
(metadata-file (build-jail-config-value effective-config 'metadata-file #f))
(mounted '()))
(dynamic-wind
(lambda ()
(for-each (lambda (directory)
(mkdir-p (jail-root-target jail-root directory)))
directories)
(when (and jail-user jail-home)
(mkdir-p (jail-root-target jail-root jail-home)))
(build-jail-effective-directories effective-config))
(for-each (lambda (spec)
(ensure-jail-mount-target jail-root spec))
mounts)
mount-specs)
(write-file script-path script)
(write-file runner-path (build-jail-runner-script effective-config))
(chmod script-path #o555)
(chmod runner-path #o555)
(when metadata-file
(mkdir-p (dirname metadata-file))
(write-file metadata-file
(object->string
(build-jail-run-metadata executor effective-config
jail-root jail-name script mount-specs))))
(for-each (lambda (spec)
(set! mounted (cons (mount-jail-spec executor jail-root spec)
mounted)))
mounts))
mount-specs))
(lambda ()
(let ((status
(system* "sh" "-c"
(string-append
(jail-sudo-command executor)
" jail -c path=" (shell-quote jail-root)
" name=" (shell-quote jail-name)
" host.hostname=" (shell-quote jail-name)
" exec.clean"
(if jail-user
(string-append " exec.jail_user=" (shell-quote jail-user))
"")
" command=/bin/sh /build.sh"))))
(build-jail-command-string executor jail-root jail-name
effective-config "/run.sh"))))
(unless (zero? status)
(error "jail build command failed" jail-name status))))
(lambda ()
@@ -347,7 +641,8 @@
(string-append "umount "
(shell-quote mountpoint)))))
mounted)
(delete-path-if-exists jail-root)))))
(unless (build-jail-config-value effective-config 'keep-root? #f)
(delete-path-if-exists jail-root))))))
(define copy-build-mounted-host-paths
'("/bin"
@@ -372,12 +667,12 @@
"/frx"
"/etc"))
(define (copy-build-jail-mounts output-path)
(append (map (lambda (path)
(jail-nullfs-mount path path 'ro))
(filter file-exists? copy-build-mounted-host-paths))
(list (jail-devfs-mount "/dev")
(jail-nullfs-mount output-path "/out" 'rw))))
(define (host-path->jail-mount-pair path)
(cons path path))
(define (copy-build-read-only-mounts)
(map host-path->jail-mount-pair
(filter file-exists? copy-build-mounted-host-paths)))
(define (native-build-extra-host-paths common)
(let ((candidate-paths (filter identity
@@ -389,13 +684,30 @@
native-build-mounted-host-paths))))
candidate-paths)))
(define (native-build-jail-mounts common build-root)
(append (map (lambda (path)
(jail-nullfs-mount path path 'ro))
(append (filter file-exists? native-build-mounted-host-paths)
(native-build-extra-host-paths common)))
(list (jail-devfs-mount "/dev")
(jail-nullfs-mount build-root build-root 'rw))))
(define (native-build-read-only-mounts common)
(map host-path->jail-mount-pair
(append (filter file-exists? native-build-mounted-host-paths)
(native-build-extra-host-paths common))))
(define (copy-build-jail-config package output-path)
(build-jail-config
#:name (string-append "fruix-copy-" (freebsd-package-name package))
#:read-only-mounts (copy-build-read-only-mounts)
#:writable-mounts (list (cons output-path "/out"))
#:mounts (list (jail-devfs-mount "/dev"))
#:workdir "/out"
#:directories '("/tmp" "/out" "/dev")
#:metadata-file (string-append output-path "/.fruix-build-jail.scm")))
(define (native-build-jail-config common build-root log-file)
(build-jail-config
#:name "fruix-native"
#:read-only-mounts (native-build-read-only-mounts common)
#:writable-mounts (list (cons build-root build-root))
#:mounts (list (jail-devfs-mount "/dev"))
#:workdir build-root
#:directories '("/tmp" "/dev")
#:metadata-file (string-append log-file ".jail.scm")))
(define (copy-plan-entry-script entry)
(match entry
@@ -436,10 +748,7 @@
(define (run-copy-build-in-jail executor package output-path)
(run-script-in-temporary-jail executor
(copy-build-jail-script package)
#:name (string-append "fruix-copy-"
(freebsd-package-name package))
#:mounts (copy-build-jail-mounts output-path)
#:directories '("/tmp" "/out" "/dev")))
#:config (copy-build-jail-config package output-path)))
(define (run-command/log-with-executor executor common build-root log-file command)
(mkdir-p (dirname log-file))
@@ -447,9 +756,7 @@
((jail)
(run-script-in-temporary-jail executor
(native-command-jail-script command log-file)
#:name "fruix-native"
#:mounts (native-build-jail-mounts common build-root)
#:directories '("/tmp" "/dev")))
#:config (native-build-jail-config common build-root log-file)))
(else
(let ((status (system* "sh" "-c" (string-append command " >" log-file " 2>&1"))))
(unless (zero? status)
+20 -5
View File
@@ -95,14 +95,29 @@
(working-directory . ,working-directory)))))
(define* (jail-native-build-executor #:key (name "jail")
host-name root-directory sudo-command)
host-name root-directory sudo-command
(network? #f)
(env '())
user home workdir
(read-only-mounts '())
(writable-mounts '())
(directories '())
(keep-root? #f))
(native-build-executor
#:kind 'jail
#:name name
#:properties (filter-map identity
`((host-name . ,host-name)
(root-directory . ,root-directory)
(sudo-command . ,sudo-command)))))
#:properties `((host-name . ,host-name)
(root-directory . ,root-directory)
(sudo-command . ,sudo-command)
(network? . ,network?)
(env . ,env)
(user . ,user)
(home . ,home)
(workdir . ,workdir)
(read-only-mounts . ,read-only-mounts)
(writable-mounts . ,writable-mounts)
(directories . ,directories)
(keep-root? . ,keep-root?))))
(define* (ssh-guest-native-build-executor #:key (name "ssh-guest")
transport orchestrator
+88 -20
View File
@@ -11,8 +11,6 @@
(@@ (fruix system freebsd build) default-copy-build-executor))
(define default-native-build-executor
(@@ (fruix system freebsd build) default-native-build-executor))
(define jail-nullfs-mount
(@@ (fruix system freebsd build) jail-nullfs-mount))
(define jail-devfs-mount
(@@ (fruix system freebsd build) jail-devfs-mount))
(define run-script-in-temporary-jail
@@ -21,6 +19,14 @@
(@@ (fruix system freebsd build) run-command/log-with-executor))
(define native-install-command-string
(@@ (fruix system freebsd build) native-install-command-string))
(define build-jail-config
(@@ (fruix system freebsd build) build-jail-config))
(define build-jail-config-ref
(@@ (fruix system freebsd build) build-jail-config-ref))
(define build-jail-base-config
(@@ (fruix system freebsd build) build-jail-base-config))
(define build-jail-command-string
(@@ (fruix system freebsd build) build-jail-command-string))
(define (trim text)
(string-trim-both text))
@@ -37,32 +43,91 @@
(if (jail-build-available?) 'jail 'host)
(native-build-executor-kind executor)))
(let* ((config (build-jail-base-config (jail-native-build-executor)))
(env (build-jail-config-ref config 'env '()))
(path-entry (assoc "PATH" env))
(tmpdir-entry (assoc "TMPDIR" env)))
(test-equal "jail build config disables network by default"
#f
(build-jail-config-ref config 'network? #t))
(test-equal "jail build config defaults to /tmp workdir"
"/tmp"
(build-jail-config-ref config 'workdir #f))
(test-equal "jail build config exposes minimal PATH"
"/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin"
(and path-entry (cdr path-entry)))
(test-equal "jail build config sets TMPDIR"
"/tmp"
(and tmpdir-entry (cdr tmpdir-entry))))
(let* ((command (build-jail-command-string
(jail-native-build-executor)
"/tmp/fruix-jail-root"
"fruix-test"
(build-jail-base-config (jail-native-build-executor))
"/run.sh"))
(networked-command (build-jail-command-string
(jail-native-build-executor #:network? #t)
"/tmp/fruix-jail-root"
"fruix-test"
(build-jail-base-config (jail-native-build-executor #:network? #t))
"/run.sh")))
(test-assert "jail command disables IPv4 and IPv6 by default"
(and (string-contains command " ip4=disable")
(string-contains command " ip6=disable")))
(test-assert "jail command can opt into network access"
(and (not (string-contains networked-command " ip4=disable"))
(not (string-contains networked-command " ip6=disable")))))
(when (jail-build-available?)
(let* ((executor (default-copy-build-executor))
(output-dir (mktemp-directory "/tmp/fruix-build-jails-copy.XXXXXX"))
(result-file (string-append output-dir "/result"))
(user-file (string-append output-dir "/user")))
(run-script-in-temporary-jail
executor
(string-append "#!/bin/sh\n"
"set -eu\n"
"printf ok > /out/result\n"
"id -un > /out/user\n")
#:name "fruix-build-jails-copy"
#:mounts (list (jail-nullfs-mount "/bin" "/bin" 'ro)
(jail-nullfs-mount "/lib" "/lib" 'ro)
(jail-nullfs-mount "/libexec" "/libexec" 'ro)
(jail-nullfs-mount "/usr" "/usr" 'ro)
(jail-nullfs-mount "/etc" "/etc" 'ro)
(jail-devfs-mount "/dev")
(jail-nullfs-mount output-dir "/out" 'rw))
#:directories '("/tmp" "/dev" "/out"))
(user-file (string-append output-dir "/user"))
(env-file (string-append output-dir "/env"))
(pwd-file (string-append output-dir "/pwd"))
(config (build-jail-config
#:name "fruix-build-jails-copy"
#:read-only-mounts (list (cons "/bin" "/bin")
(cons "/lib" "/lib")
(cons "/libexec" "/libexec")
(cons "/usr" "/usr")
(cons "/etc" "/etc"))
#:writable-mounts (list (cons output-dir "/out"))
#:mounts (list (jail-devfs-mount "/dev"))
#:workdir "/out"
#:directories '("/tmp" "/dev" "/out"))))
(dynamic-wind
(lambda ()
(setenv "FRUIX_BUILD_JAIL_TEST_SECRET" "should-not-leak"))
(lambda ()
(run-script-in-temporary-jail
executor
(string-append "#!/bin/sh\n"
"set -eu\n"
"printf ok > /out/result\n"
"id -un > /out/user\n"
"env | sort > /out/env\n"
"pwd > /out/pwd\n")
#:config config))
(lambda ()
(unsetenv "FRUIX_BUILD_JAIL_TEST_SECRET")))
(test-equal "generic jail runner writes result"
"ok"
(trim (command-output "cat" result-file)))
(test-equal "generic jail runner uses current user ownership context"
(trim (command-output "id" "-un"))
(trim (command-output "cat" user-file)))))
(trim (command-output "cat" user-file)))
(test-equal "generic jail runner uses configured workdir"
"/out"
(trim (command-output "cat" pwd-file)))
(let ((env-text (command-output "cat" env-file)))
(test-assert "generic jail runner drops inherited host environment"
(not (string-contains env-text "FRUIX_BUILD_JAIL_TEST_SECRET=")))
(test-assert "generic jail runner preserves whitelisted PATH"
(string-contains env-text "PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin"))
(test-assert "generic jail runner preserves whitelisted TMPDIR"
(string-contains env-text "TMPDIR=/tmp")))))
(let* ((executor (default-native-build-executor))
(build-root (mktemp-directory "/tmp/fruix-build-jails-native.XXXXXX"))
@@ -80,6 +145,9 @@
(run-command/log-with-executor executor common build-root log-file
"make -C /usr/src -V .CURDIR")
(test-assert "native build executor can run make with mounted source tree"
(string-contains (trim (command-output "cat" log-file)) "/usr/src")))
(string-contains (trim (command-output "cat" log-file)) "/usr/src"))
(when (eq? (native-build-executor-kind executor) 'jail)
(test-assert "native build executor records jail metadata"
(file-exists? (string-append log-file ".jail.scm")))))
(test-end "build-jails")