summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMaxim Cournoyer <maxim@guixotic.coop>2025-10-17 23:12:27 +0900
committerMaxim Cournoyer <maxim@guixotic.coop>2025-10-29 11:34:28 +0900
commit1eccea7ffb7eac43670d5fd76e8afa8ecfe6b0b9 (patch)
tree1f40d89649f5449aa3524e2301c186ca94ea73d3
parent3966f7629723c68e49b66fdf05feab901f8741ac (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.scm181
-rw-r--r--guix/build/syscalls.scm53
-rw-r--r--tests/syscalls.scm36
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."
1170Turning finalization off shuts down the finalization thread as a side effect." 1171Turning 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
1176useful 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
1188to avoid the creation new garbage collection thread. Note that pre-existing
1189GC 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
1200not run. Note that pre-existing GC marker threads are only disabled, not
1201terminated. This also leaves the signal handling thread to be disabled by
1202another 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
1257spawned during execution of the child. `clone' is called with FLAGS. CHILD
1258is a thunk to run in the child process. PARENT is procedure that accepts the
1259child 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
1261memory (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"