diff options
| author | Maxim Cournoyer <maxim@guixotic.coop> | 2025-10-17 23:12:27 +0900 |
|---|---|---|
| committer | Maxim Cournoyer <maxim@guixotic.coop> | 2025-10-29 11:34:28 +0900 |
| commit | 1eccea7ffb7eac43670d5fd76e8afa8ecfe6b0b9 (patch) | |
| tree | 1f40d89649f5449aa3524e2301c186ca94ea73d3 | |
| parent | 3966f7629723c68e49b66fdf05feab901f8741ac (diff) | |
build/syscalls: Introduce new safe-clone and use it.
* guix/build/syscalls.scm (without-automatic-finalization): Accept multiple
expressions.
(without-garbage-collection): New syntax.
(without-threads): Likewise.
(ensure-signal-delivery-thread, safe-clone): New procedures.
* tests/syscalls.scm: ("clone and unshare triggers EINVAL")
("safe-clone and unshare succeeds"): New tests.
* gnu/build/linux-container.scm (run-container): Adjust to use 'safe-clone'.
Relates-to: #1169
Change-Id: I044c11a899e24e547a7aed97f30c8e7250ab5363
| -rw-r--r-- | gnu/build/linux-container.scm | 181 | ||||
| -rw-r--r-- | guix/build/syscalls.scm | 53 | ||||
| -rw-r--r-- | tests/syscalls.scm | 36 |
3 files changed, 172 insertions, 98 deletions
diff --git a/gnu/build/linux-container.scm b/gnu/build/linux-container.scm index 25890ec0a13..ff5449d0b0f 100644 --- a/gnu/build/linux-container.scm +++ b/gnu/build/linux-container.scm | |||
| @@ -263,100 +263,93 @@ that host UIDs (respectively GIDs) map to in the namespace." | |||
| 263 | ;; child process blocks until the parent writes to it. | 263 | ;; child process blocks until the parent writes to it. |
| 264 | (match (socketpair PF_UNIX (logior SOCK_CLOEXEC SOCK_STREAM) 0) | 264 | (match (socketpair PF_UNIX (logior SOCK_CLOEXEC SOCK_STREAM) 0) |
| 265 | ((child . parent) | 265 | ((child . parent) |
| 266 | (let ((flags (namespaces->bit-mask namespaces))) | 266 | (safe-clone |
| 267 | (match (clone flags) | 267 | (namespaces->bit-mask namespaces) |
| 268 | (0 | 268 | (lambda () |
| 269 | ;; Inhibit thread creation until after the unshare call. | 269 | (call-with-clean-exit |
| 270 | (gc-disable) | 270 | (lambda () |
| 271 | (call-with-clean-exit | 271 | (close-port parent) |
| 272 | (lambda () | 272 | ;; Wait for parent to set things up. |
| 273 | (close-port parent) | 273 | (match (read child) |
| 274 | ;; Wait for parent to set things up. | 274 | ('ready |
| 275 | (match (read child) | 275 | (purify-environment) |
| 276 | ('ready | 276 | (when (and (memq 'mnt namespaces) |
| 277 | (purify-environment) | 277 | (not (string=? root "/"))) |
| 278 | (when (and (memq 'mnt namespaces) | 278 | (catch #t |
| 279 | (not (string=? root "/"))) | 279 | (lambda () |
| 280 | (catch #t | 280 | (mount-file-systems root mounts |
| 281 | (lambda () | 281 | #:mount-/proc? (memq 'pid namespaces) |
| 282 | (mount-file-systems root mounts | 282 | #:mount-/sys? (memq 'net |
| 283 | #:mount-/proc? (memq 'pid namespaces) | 283 | namespaces) |
| 284 | #:mount-/sys? (memq 'net | 284 | #:populate-file-system |
| 285 | namespaces) | 285 | (lambda () |
| 286 | #:populate-file-system | 286 | (populate-file-system) |
| 287 | (lambda () | 287 | (when (and (memq 'net namespaces) |
| 288 | (populate-file-system) | 288 | loopback-network?) |
| 289 | (when (and (memq 'net namespaces) | 289 | (set-network-interface-up "lo") |
| 290 | loopback-network?) | 290 | |
| 291 | (set-network-interface-up "lo") | 291 | ;; When isolated from the |
| 292 | 292 | ;; network, provide a minimal | |
| 293 | ;; When isolated from the | 293 | ;; /etc/hosts to resolve |
| 294 | ;; network, provide a minimal | 294 | ;; "localhost". |
| 295 | ;; /etc/hosts to resolve | 295 | (mkdir-p "/etc") |
| 296 | ;; "localhost". | 296 | (call-with-output-file "/etc/hosts" |
| 297 | (mkdir-p "/etc") | 297 | (lambda (port) |
| 298 | (call-with-output-file "/etc/hosts" | 298 | (display "127.0.0.1 localhost\n" port) |
| 299 | (lambda (port) | 299 | (chmod port #o444))))) |
| 300 | (display "127.0.0.1 localhost\n" port) | 300 | #:writable-root? |
| 301 | (chmod port #o444))))) | 301 | (or writable-root? |
| 302 | #:writable-root? | 302 | (not (memq 'mnt namespaces))))) |
| 303 | (or writable-root? | 303 | (lambda args |
| 304 | (not (memq 'mnt namespaces))))) | 304 | ;; Forward the exception to the parent process. |
| 305 | (lambda args | 305 | ;; FIXME: SRFI-35 conditions and non-trivial objects |
| 306 | ;; Forward the exception to the parent process. | 306 | ;; cannot be 'read' so they shouldn't be written as is. |
| 307 | ;; FIXME: SRFI-35 conditions and non-trivial objects | 307 | (write args child) |
| 308 | ;; cannot be 'read' so they shouldn't be written as is. | 308 | (primitive-exit 3)))) |
| 309 | (write args child) | 309 | |
| 310 | (primitive-exit 3)))) | 310 | (when (and lock-mounts? |
| 311 | 311 | (memq 'mnt namespaces) | |
| 312 | (when (and lock-mounts? | 312 | (memq 'user namespaces)) |
| 313 | (memq 'mnt namespaces) | 313 | ;; Create a new mount namespace owned by a new user |
| 314 | (memq 'user namespaces)) | 314 | ;; namespace to "lock" together previous mounts, such that |
| 315 | ;; Create a new mount namespace owned by a new user | 315 | ;; they cannot be unmounted or remounted separately--see |
| 316 | ;; namespace to "lock" together previous mounts, such that | 316 | ;; mount_namespaces(7). |
| 317 | ;; they cannot be unmounted or remounted separately--see | 317 | (let ((uid (getuid)) (gid (getgid))) |
| 318 | ;; mount_namespaces(7). | 318 | (unshare (logior CLONE_NEWUSER CLONE_NEWNS)) |
| 319 | ;; | 319 | (when (file-exists? "/proc/self") |
| 320 | ;; Note: at this point, the process is single-threaded (no | 320 | (initialize-user-namespace (getpid) |
| 321 | ;; GC mark threads, no finalization thread, etc.) which is | 321 | host-uids |
| 322 | ;; why unshare(CLONE_NEWUSER) can be used. | 322 | #:host-uid uid |
| 323 | (let ((uid (getuid)) (gid (getgid))) | 323 | #:host-gid gid |
| 324 | (unshare (logior CLONE_NEWUSER CLONE_NEWNS)) | 324 | #:guest-uid guest-uid |
| 325 | (gc-enable) | 325 | #:guest-gid guest-gid)))) |
| 326 | (when (file-exists? "/proc/self") | 326 | |
| 327 | (initialize-user-namespace (getpid) | 327 | ;; TODO: Manage capabilities. |
| 328 | host-uids | 328 | (write 'ready child) |
| 329 | #:host-uid uid | 329 | (close-port child) |
| 330 | #:host-gid gid | 330 | (thunk)) |
| 331 | #:guest-uid guest-uid | 331 | (_ ;parent died or something |
| 332 | #:guest-gid guest-gid)))) | 332 | (primitive-exit 2)))))) |
| 333 | 333 | (lambda (pid) | |
| 334 | ;; TODO: Manage capabilities. | 334 | (close-port child) |
| 335 | (write 'ready child) | 335 | (when (memq 'user namespaces) |
| 336 | (close-port child) | 336 | (initialize-user-namespace pid host-uids |
| 337 | (thunk)) | 337 | #:guest-uid guest-uid |
| 338 | (_ ;parent died or something | 338 | #:guest-gid guest-gid)) |
| 339 | (primitive-exit 2)))))) | 339 | ;; TODO: Initialize cgroups. |
| 340 | (pid | 340 | (write 'ready parent) |
| 341 | (close-port child) | 341 | (newline parent) |
| 342 | (when (memq 'user namespaces) | 342 | |
| 343 | (initialize-user-namespace pid host-uids | 343 | ;; Check whether the child process' setup phase succeeded. |
| 344 | #:guest-uid guest-uid | 344 | (let ((message (read parent))) |
| 345 | #:guest-gid guest-gid)) | 345 | (close-port parent) |
| 346 | ;; TODO: Initialize cgroups. | 346 | (match message |
| 347 | (write 'ready parent) | 347 | ('ready ;success |
| 348 | (newline parent) | 348 | pid) |
| 349 | 349 | (((? symbol? key) args ...) ;exception | |
| 350 | ;; Check whether the child process' setup phase succeeded. | 350 | (apply throw key args)) |
| 351 | (let ((message (read parent))) | 351 | (_ ;unexpected termination |
| 352 | (close-port parent) | 352 | #f)))))))) |
| 353 | (match message | ||
| 354 | ('ready ;success | ||
| 355 | pid) | ||
| 356 | (((? symbol? key) args ...) ;exception | ||
| 357 | (apply throw key args)) | ||
| 358 | (_ ;unexpected termination | ||
| 359 | #f))))))))) | ||
| 360 | 353 | ||
| 361 | ;; FIXME: This is copied from (guix utils), which we cannot use because it | 354 | ;; FIXME: This is copied from (guix utils), which we cannot use because it |
| 362 | ;; would pull (guix config) and all. | 355 | ;; would pull (guix config) and all. |
diff --git a/guix/build/syscalls.scm b/guix/build/syscalls.scm index 3106e4e3d6d..d40b1ae5d93 100644 --- a/guix/build/syscalls.scm +++ b/guix/build/syscalls.scm | |||
| @@ -150,6 +150,7 @@ | |||
| 150 | CLONE_THREAD | 150 | CLONE_THREAD |
| 151 | CLONE_VM | 151 | CLONE_VM |
| 152 | clone | 152 | clone |
| 153 | safe-clone | ||
| 153 | unshare | 154 | unshare |
| 154 | setns | 155 | setns |
| 155 | get-user-ns | 156 | get-user-ns |
| @@ -1170,17 +1171,45 @@ caller lacks root privileges." | |||
| 1170 | Turning finalization off shuts down the finalization thread as a side effect." | 1171 | Turning finalization off shuts down the finalization thread as a side effect." |
| 1171 | (->bool ((force proc) (if enabled? 1 0)))))) | 1172 | (->bool ((force proc) (if enabled? 1 0)))))) |
| 1172 | 1173 | ||
| 1173 | (define-syntax-rule (without-automatic-finalization exp) | 1174 | (define-syntax-rule (without-automatic-finalization body ...) |
| 1174 | "Turn off automatic finalization within the dynamic extent of EXP." | 1175 | "Turn off automatic finalization within the dynamic extent of BODY. This is |
| 1176 | useful to ensure there is no finalization thread." | ||
| 1175 | (let ((enabled? #t)) | 1177 | (let ((enabled? #t)) |
| 1176 | (dynamic-wind | 1178 | (dynamic-wind |
| 1177 | (lambda () | 1179 | (lambda () |
| 1178 | (set! enabled? (%set-automatic-finalization-enabled?! #f))) | 1180 | (set! enabled? (%set-automatic-finalization-enabled?! #f))) |
| 1179 | (lambda () | 1181 | (lambda () |
| 1180 | exp) | 1182 | body ...) |
| 1181 | (lambda () | 1183 | (lambda () |
| 1182 | (%set-automatic-finalization-enabled?! enabled?))))) | 1184 | (%set-automatic-finalization-enabled?! enabled?))))) |
| 1183 | 1185 | ||
| 1186 | (define-syntax-rule (without-garbage-collection body ...) | ||
| 1187 | "Turn off garbage collection within the dynamic extent of BODY. This is useful | ||
| 1188 | to avoid the creation new garbage collection thread. Note that pre-existing | ||
| 1189 | GC marker threads are only disabled, not terminated." | ||
| 1190 | (dynamic-wind | ||
| 1191 | (lambda () | ||
| 1192 | (gc-disable)) | ||
| 1193 | (lambda () | ||
| 1194 | body ...) | ||
| 1195 | (lambda () | ||
| 1196 | (gc-enable)))) | ||
| 1197 | |||
| 1198 | (define-syntax-rule (without-threads body ...) | ||
| 1199 | "Ensure the Guile finalizer thread is stopped and that garbage collection does | ||
| 1200 | not run. Note that pre-existing GC marker threads are only disabled, not | ||
| 1201 | terminated. This also leaves the signal handling thread to be disabled by | ||
| 1202 | another means, since there is no Guile API to do so." | ||
| 1203 | ;; Note: the three kind of threads that Guile can spawn are the finalization | ||
| 1204 | ;; thread, the signal thread, or the GC marker threads. | ||
| 1205 | (without-automatic-finalization | ||
| 1206 | (without-garbage-collection body ...))) | ||
| 1207 | |||
| 1208 | (define (ensure-signal-delivery-thread) | ||
| 1209 | "Ensure the signal delivery thread is spawned and its state set | ||
| 1210 | to 'RUNNING'. This is valid as of the implementation as of Guile 3.0.9." | ||
| 1211 | (sigaction SIGUSR1)) ;could be any signal | ||
| 1212 | |||
| 1184 | ;; The libc interface to sys_clone is not useful for Scheme programs, so the | 1213 | ;; The libc interface to sys_clone is not useful for Scheme programs, so the |
| 1185 | ;; low-level system call is wrapped instead. The 'syscall' function is | 1214 | ;; low-level system call is wrapped instead. The 'syscall' function is |
| 1186 | ;; declared in <unistd.h> as a variadic function; in practice, it expects 6 | 1215 | ;; declared in <unistd.h> as a variadic function; in practice, it expects 6 |
| @@ -1223,6 +1252,24 @@ are shared between the parent and child processes." | |||
| 1223 | (list err)) | 1252 | (list err)) |
| 1224 | ret))))) | 1253 | ret))))) |
| 1225 | 1254 | ||
| 1255 | (define (safe-clone flags child parent) | ||
| 1256 | "This is a raw clone syscall wrapper that ensures no Guile thread will be | ||
| 1257 | spawned during execution of the child. `clone' is called with FLAGS. CHILD | ||
| 1258 | is a thunk to run in the child process. PARENT is procedure that accepts the | ||
| 1259 | child PID as argument. This is useful in many contexts, such as when calling | ||
| 1260 | `unshare' or async-unsafe procedures in the child when the parent process | ||
| 1261 | memory (CLONE_VM) or threads (CLONE_THREAD) are shared with it." | ||
| 1262 | ;; TODO: Contribute `clone' to Guile, and handle these complications there, | ||
| 1263 | ;; similarly to how it's handled for scm_fork in posix.c. | ||
| 1264 | |||
| 1265 | ;; XXX: This is a hack: as of Guile 3.0.9, by starting the signal delivery | ||
| 1266 | ;; thread in the parent, its state will be known as RUNNING, and the child | ||
| 1267 | ;; won't attempt to start it itself. | ||
| 1268 | (ensure-signal-delivery-thread) | ||
| 1269 | (match (clone flags) | ||
| 1270 | (0 (without-threads (child))) | ||
| 1271 | (pid (parent pid)))) | ||
| 1272 | |||
| 1226 | (define (thread-count) | 1273 | (define (thread-count) |
| 1227 | "Return the complete thread count of the current process. Unlike | 1274 | "Return the complete thread count of the current process. Unlike |
| 1228 | `all-threads', this also counts the Guile signal delivery, and finalizer | 1275 | `all-threads', this also counts the Guile signal delivery, and finalizer |
diff --git a/tests/syscalls.scm b/tests/syscalls.scm index 879c3e4f254..a0483e68f08 100644 --- a/tests/syscalls.scm +++ b/tests/syscalls.scm | |||
| @@ -3,6 +3,7 @@ | |||
| 3 | ;;; Copyright © 2015 David Thompson <davet@gnu.org> | 3 | ;;; Copyright © 2015 David Thompson <davet@gnu.org> |
| 4 | ;;; Copyright © 2020 Simon South <simon@simonsouth.net> | 4 | ;;; Copyright © 2020 Simon South <simon@simonsouth.net> |
| 5 | ;;; Copyright © 2020 Mathieu Othacehe <m.othacehe@gmail.com> | 5 | ;;; Copyright © 2020 Mathieu Othacehe <m.othacehe@gmail.com> |
| 6 | ;;; Copyright © 2025 Maxim Cournoyer <maxim@guixotic.coop> | ||
| 6 | ;;; | 7 | ;;; |
| 7 | ;;; This file is part of GNU Guix. | 8 | ;;; This file is part of GNU Guix. |
| 8 | ;;; | 9 | ;;; |
| @@ -29,7 +30,8 @@ | |||
| 29 | #:use-module (srfi srfi-71) | 30 | #:use-module (srfi srfi-71) |
| 30 | #:use-module (system foreign) | 31 | #:use-module (system foreign) |
| 31 | #:use-module ((ice-9 ftw) #:select (scandir)) | 32 | #:use-module ((ice-9 ftw) #:select (scandir)) |
| 32 | #:use-module (ice-9 match)) | 33 | #:use-module (ice-9 match) |
| 34 | #:use-module (ice-9 threads)) | ||
| 33 | 35 | ||
| 34 | ;; Test the (guix build syscalls) module, although there's not much that can | 36 | ;; Test the (guix build syscalls) module, although there's not much that can |
| 35 | ;; actually be tested without being root. | 37 | ;; actually be tested without being root. |
| @@ -158,6 +160,38 @@ | |||
| 158 | (lambda args | 160 | (lambda args |
| 159 | (system-error-errno args)))) | 161 | (system-error-errno args)))) |
| 160 | 162 | ||
| 163 | (define child-thunk | ||
| 164 | (lambda () | ||
| 165 | (gc) ;spawn GC threads | ||
| 166 | (primitive-exit | ||
| 167 | (catch 'system-error | ||
| 168 | (lambda () | ||
| 169 | (unshare CLONE_THREAD) | ||
| 170 | 0) ;no error | ||
| 171 | (lambda args | ||
| 172 | (system-error-errno args)))))) | ||
| 173 | |||
| 174 | (define parent-proc | ||
| 175 | (lambda (pid) | ||
| 176 | (match (waitpid pid) | ||
| 177 | ((_ . status) | ||
| 178 | (status:exit-val status))))) | ||
| 179 | |||
| 180 | (unless perform-container-tests? | ||
| 181 | (test-skip 1)) | ||
| 182 | (test-equal "clone and unshare triggers EINVAL" | ||
| 183 | EINVAL | ||
| 184 | (match (clone (logior CLONE_NEWUSER SIGCHLD)) | ||
| 185 | (0 (child-thunk)) | ||
| 186 | (pid (parent-proc pid)))) | ||
| 187 | |||
| 188 | (unless perform-container-tests? | ||
| 189 | (test-skip 1)) | ||
| 190 | (test-equal "safe-clone and unshare succeeds" | ||
| 191 | 0 | ||
| 192 | (safe-clone (logior CLONE_NEWUSER SIGCHLD) | ||
| 193 | child-thunk parent-proc)) | ||
| 194 | |||
| 161 | (unless perform-container-tests? | 195 | (unless perform-container-tests? |
| 162 | (test-skip 1)) | 196 | (test-skip 1)) |
| 163 | (test-assert "setns" | 197 | (test-assert "setns" |
