Skip to content

Commit 1f2aeaf

Browse files
committed
Optionally return the inner pid from spawn()
bubblewrap does not support forwarding signals yet, see containers/bubblewrap#586. As a workaround, we need to make sure we send our signals to the inner process. To make this work, we create a pipe, pass it through to the subprocess, and prefix with a bash command that writes its pid to the pipe before exec-ing the actual command. The other thing we get from this is that we can register the inner pid as a scope which makes the systemctl status output for the scopes we create a lot more useful.
1 parent 6c30552 commit 1f2aeaf

3 files changed

Lines changed: 58 additions & 20 deletions

File tree

mkosi/qemu.py

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import os
1313
import random
1414
import shutil
15+
import signal
1516
import socket
1617
import struct
1718
import subprocess
@@ -38,7 +39,7 @@
3839
from mkosi.log import ARG_DEBUG, die
3940
from mkosi.mounts import finalize_source_mounts
4041
from mkosi.partition import finalize_root, find_partitions
41-
from mkosi.run import SD_LISTEN_FDS_START, AsyncioThread, find_binary, fork_and_wait, run, spawn
42+
from mkosi.run import SD_LISTEN_FDS_START, AsyncioThread, find_binary, fork_and_wait, kill, run, spawn
4243
from mkosi.sandbox import Mount
4344
from mkosi.tree import copy_tree, rmtree
4445
from mkosi.types import PathString
@@ -274,15 +275,15 @@ def start_swtpm(config: Config) -> Iterator[Path]:
274275
cmdline,
275276
pass_fds=(sock.fileno(),),
276277
sandbox=config.sandbox(mounts=[Mount(state, state)]),
277-
) as proc:
278+
) as (proc, innerpid):
278279
allocate_scope(
279280
config,
280281
name=f"mkosi-swtpm-{config.machine_or_name()}",
281-
pid=proc.pid,
282+
pid=innerpid,
282283
description=f"swtpm for {config.machine_or_name()}",
283284
)
284285
yield path
285-
proc.terminate()
286+
kill(proc, innerpid, signal.SIGTERM)
286287

287288

288289
def find_virtiofsd(*, tools: Path = Path("/")) -> Optional[Path]:
@@ -354,15 +355,15 @@ def start_virtiofsd(config: Config, directory: PathString, *, name: str, selinux
354355
mounts=[Mount(directory, directory)],
355356
options=["--uid", "0", "--gid", "0", "--cap-add", "all"],
356357
),
357-
) as proc:
358+
) as (proc, innerpid):
358359
allocate_scope(
359360
config,
360361
name=f"mkosi-virtiofsd-{name}",
361-
pid=proc.pid,
362+
pid=innerpid,
362363
description=f"virtiofsd for {directory}",
363364
)
364365
yield path
365-
proc.terminate()
366+
kill(proc, innerpid, signal.SIGTERM)
366367

367368

368369
@contextlib.contextmanager
@@ -442,15 +443,16 @@ def start_journal_remote(config: Config, sockfd: int) -> Iterator[None]:
442443
# If all logs go into a single file, disable compact mode to allow for journal files exceeding 4G.
443444
env={"SYSTEMD_JOURNAL_COMPACT": "0" if config.forward_journal.suffix == ".journal" else "1"},
444445
foreground=False,
445-
) as proc:
446+
) as (proc, innerpid):
446447
allocate_scope(
447448
config,
448449
name=f"mkosi-journal-remote-{config.machine_or_name()}",
449-
pid=proc.pid,
450+
pid=innerpid,
450451
description=f"mkosi systemd-journal-remote for {config.machine_or_name()}",
451452
)
452453
yield
453-
proc.terminate()
454+
kill(proc, innerpid, signal.SIGTERM)
455+
454456

455457

456458
@contextlib.contextmanager
@@ -1097,7 +1099,7 @@ def add_virtiofs_mount(
10971099
log=False,
10981100
foreground=True,
10991101
sandbox=config.sandbox(network=True, devices=True, relaxed=True),
1100-
) as qemu:
1102+
) as (proc, innerpid):
11011103
# We have to close these before we wait for qemu otherwise we'll deadlock as qemu will never exit.
11021104
for fd in qemu_device_fds.values():
11031105
os.close(fd)
@@ -1106,12 +1108,12 @@ def add_virtiofs_mount(
11061108
allocate_scope(
11071109
config,
11081110
name=name,
1109-
pid=qemu.pid,
1111+
pid=innerpid,
11101112
description=f"mkosi Virtual Machine {name}",
11111113
)
1112-
register_machine(config, qemu.pid, fname)
1114+
register_machine(config, innerpid, fname)
11131115

1114-
if qemu.wait() == 0 and (status := int(notifications.get("EXIT_STATUS", 0))):
1116+
if proc.wait() == 0 and (status := int(notifications.get("EXIT_STATUS", 0))):
11151117
raise subprocess.CalledProcessError(status, cmdline)
11161118

11171119

mkosi/run.py

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,8 @@ def run(
159159
preexec_fn=preexec_fn,
160160
success_exit_status=success_exit_status,
161161
sandbox=sandbox,
162-
) as process:
162+
innerpid=False,
163+
) as (process, _):
163164
out, err = process.communicate(input)
164165

165166
return CompletedProcess(cmdline, process.returncode, out, err)
@@ -182,7 +183,8 @@ def spawn(
182183
preexec_fn: Optional[Callable[[], None]] = None,
183184
success_exit_status: Sequence[int] = (0,),
184185
sandbox: AbstractContextManager[Sequence[PathString]] = contextlib.nullcontext([]),
185-
) -> Iterator[Popen]:
186+
innerpid: bool = True,
187+
) -> Iterator[tuple[Popen, int]]:
186188
assert sorted(set(pass_fds)) == list(pass_fds)
187189

188190
cmdline = [os.fspath(x) for x in cmdline]
@@ -274,6 +276,16 @@ def preexec() -> None:
274276
# command.
275277
prefix += ["sh", "-c", f"LISTEN_FDS={len(pass_fds)} LISTEN_PID=$$ exec $0 \"$@\""]
276278

279+
if prefix and innerpid:
280+
r, w = os.pipe2(os.O_CLOEXEC)
281+
q = fcntl.fcntl(w, fcntl.F_DUPFD_CLOEXEC, len(pass_fds) + 1)
282+
os.close(w)
283+
w = q
284+
# dash doesn't support working with file descriptors higher than 9 so make sure we use bash.
285+
prefix += ["bash", "-c", f"echo $$ >&{w} && exec {w}>&- && exec $0 \"$@\""]
286+
else:
287+
r, w = (None, None)
288+
277289
try:
278290
with subprocess.Popen(
279291
prefix + cmdline,
@@ -285,15 +297,24 @@ def preexec() -> None:
285297
group=group,
286298
# pass_fds only comes into effect after python has invoked the preexec function, so we make sure that
287299
# pass_fds contains the file descriptors to keep open after we've done our transformation in preexec().
288-
pass_fds=[SD_LISTEN_FDS_START + i for i in range(len(pass_fds))],
300+
pass_fds=[SD_LISTEN_FDS_START + i for i in range(len(pass_fds))] + ([w] if w else []),
289301
env=env,
290302
cwd=cwd,
291303
preexec_fn=preexec,
292304
) as proc:
305+
if w:
306+
os.close(w)
307+
pid = proc.pid
293308
try:
294-
yield proc
309+
if r:
310+
with open(r) as f:
311+
s = f.read()
312+
if s:
313+
pid = int(s)
314+
315+
yield proc, pid
295316
except BaseException:
296-
proc.terminate()
317+
kill(proc, pid, signal.SIGTERM)
297318
raise
298319
finally:
299320
returncode = proc.wait()
@@ -342,6 +363,17 @@ def find_binary(*names: PathString, root: Path = Path("/")) -> Optional[Path]:
342363
return None
343364

344365

366+
def kill(process: Popen, innerpid: int, signal: int) -> None:
367+
process.poll()
368+
if process.returncode is not None:
369+
return
370+
371+
try:
372+
os.kill(innerpid, signal)
373+
except ProcessLookupError:
374+
pass
375+
376+
345377
class AsyncioThread(threading.Thread):
346378
"""
347379
The default threading.Thread() is not interruptable, so we make our own version by using the concurrency

mkosi/user.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,11 @@ def become_root() -> None:
187187
# execute using flock so they don't execute before they can get a lock on the same temporary file, then we
188188
# unshare the user namespace and finally we unlock the temporary file, which allows the newuidmap and newgidmap
189189
# processes to execute. we then wait for the processes to finish before continuing.
190-
with flock(lock) as fd, spawn(newuidmap) as uidmap, spawn(newgidmap) as gidmap:
190+
with (
191+
flock(lock) as fd,
192+
spawn(newuidmap, innerpid=False) as (uidmap, _),
193+
spawn(newgidmap, innerpid=False) as (gidmap, _)
194+
):
191195
unshare(CLONE_NEWUSER)
192196
fcntl.flock(fd, fcntl.LOCK_UN)
193197
uidmap.wait()

0 commit comments

Comments
 (0)