]> git.ipfire.org Git - thirdparty/mkosi.git/commitdiff
Introduce Mount named tuple to pass mounts to sandbox_cmd()
authorDaan De Meyer <daan.j.demeyer@gmail.com>
Fri, 15 Mar 2024 08:59:04 +0000 (09:59 +0100)
committerDaan De Meyer <daan.j.demeyer@gmail.com>
Fri, 15 Mar 2024 11:26:26 +0000 (12:26 +0100)
No change in behavior, but this will allow us to post-process mounts
in sandbox_cmd() in later commits.

19 files changed:
mkosi/__init__.py
mkosi/archive.py
mkosi/config.py
mkosi/context.py
mkosi/distributions/debian.py
mkosi/distributions/opensuse.py
mkosi/installer/__init__.py
mkosi/installer/apt.py
mkosi/installer/dnf.py
mkosi/installer/pacman.py
mkosi/installer/zypper.py
mkosi/kmod.py
mkosi/manifest.py
mkosi/mounts.py
mkosi/partition.py
mkosi/qemu.py
mkosi/sandbox.py
mkosi/tree.py
mkosi/vmspawn.py

index 06fa4f009b44781e53e2e5cba7e4408ee0d03ce0..6e6b68a541ed00de5aa1f39a3215f10964cc7c01 100644 (file)
@@ -65,7 +65,7 @@ from mkosi.run import (
     log_process_failure,
     run,
 )
-from mkosi.sandbox import chroot_cmd, finalize_crypto_mounts, finalize_passwd_mounts
+from mkosi.sandbox import Mount, chroot_cmd, finalize_crypto_mounts, finalize_passwd_mounts
 from mkosi.tree import copy_tree, move_tree, rmtree
 from mkosi.types import PathString
 from mkosi.user import CLONE_NEWNS, INVOKING_USER, become_root, unshare
@@ -449,28 +449,31 @@ def run_sync_scripts(context: Context) -> None:
         finalize_config_json(context.config) as json,
     ):
         for script in context.config.sync_scripts:
-            options = [
+            mounts = [
                 *sources,
                 *finalize_crypto_mounts(context.config.tools()),
-                "--ro-bind", script, "/work/sync",
-                "--ro-bind", json, "/work/config.json",
-                "--chdir", "/work/src",
+                Mount(script, "/work/sync", ro=True),
+                Mount(json, "/work/config.json", ro=True),
             ]
 
             if (p := INVOKING_USER.home()).exists():
-                # We use --bind here instead of --ro-bind to keep git worktrees working which encode absolute paths to
-                # the parent git repository and might need to modify the git config in the parent git repository when
-                # submodules are in use as well.
-                options += ["--bind", p, p]
+                # We use a writable mount here to keep git worktrees working which encode absolute paths to the parent
+                # git repository and might need to modify the git config in the parent git repository when submodules
+                # are in use as well.
+                mounts += [Mount(p, p)]
             if (p := Path(f"/run/user/{INVOKING_USER.uid}")).exists():
-                options += ["--ro-bind", p, p]
+                mounts += [Mount(p, p, ro=True)]
 
             with complete_step(f"Running sync script {script}…"):
                 run(
                     ["/work/sync", "final"],
                     env=env | context.config.environment,
                     stdin=sys.stdin,
-                    sandbox=context.sandbox(network=True, options=options),
+                    sandbox=context.sandbox(
+                        network=True,
+                        mounts=mounts,
+                        options=["--dir", "/work/src", "--chdir", "/work/src"]
+                    ),
                 )
 
 
@@ -533,14 +536,15 @@ def run_prepare_scripts(context: Context, build: bool) -> None:
                     stdin=sys.stdin,
                     sandbox=context.sandbox(
                         network=True,
-                        options=sources + [
-                            "--ro-bind", script, "/work/prepare",
-                            "--ro-bind", json, "/work/config.json",
-                            "--ro-bind", cd, "/work/scripts",
-                            "--bind", context.root, context.root,
+                        mounts=[
+                            *sources,
+                            Mount(script, "/work/prepare", ro=True),
+                            Mount(json, "/work/config.json", ro=True),
+                            Mount(cd, "/work/scripts", ro=True),
+                            Mount(context.root, context.root),
                             *context.config.distribution.package_manager(context.config).mounts(context),
-                            "--chdir", "/work/src",
                         ],
+                        options=["--dir", "/work/src", "--chdir", "/work/src"],
                         scripts=hd,
                     ) + (chroot if script.suffix == ".chroot" else []),
                 )
@@ -608,21 +612,22 @@ def run_build_scripts(context: Context) -> None:
                     stdin=sys.stdin,
                     sandbox=context.sandbox(
                         network=context.config.with_network,
-                        options=sources + [
-                            "--ro-bind", script, "/work/build-script",
-                            "--ro-bind", json, "/work/config.json",
-                            "--ro-bind", cd, "/work/scripts",
-                            "--bind", context.root, context.root,
-                            "--bind", context.install_dir, "/work/dest",
-                            "--bind", context.staging, "/work/out",
+                        mounts=[
+                            *sources,
+                            Mount(script, "/work/build-script", ro=True),
+                            Mount(json, "/work/config.json", ro=True),
+                            Mount(cd, "/work/scripts", ro=True),
+                            Mount(context.root, context.root),
+                            Mount(context.install_dir, "/work/dest"),
+                            Mount(context.staging, "/work/out"),
                             *(
-                                ["--bind", os.fspath(context.config.build_dir), "/work/build"]
+                                [Mount(context.config.build_dir, "/work/build")]
                                 if context.config.build_dir
                                 else []
                             ),
                             *context.config.distribution.package_manager(context.config).mounts(context),
-                            "--chdir", "/work/src",
                         ],
+                        options=["--dir", "/work/src", "--chdir", "/work/src"],
                         scripts=hd,
                     ) + (chroot if script.suffix == ".chroot" else []),
                 )
@@ -680,15 +685,16 @@ def run_postinst_scripts(context: Context) -> None:
                     stdin=sys.stdin,
                     sandbox=context.sandbox(
                         network=context.config.with_network,
-                        options=sources + [
-                            "--ro-bind", script, "/work/postinst",
-                            "--ro-bind", json, "/work/config.json",
-                            "--ro-bind", cd, "/work/scripts",
-                            "--bind", context.root, context.root,
-                            "--bind", context.staging, "/work/out",
+                        mounts=[
+                            *sources,
+                            Mount(script, "/work/postinst", ro=True),
+                            Mount(json, "/work/config.json", ro=True),
+                            Mount(cd, "/work/scripts", ro=True),
+                            Mount(context.root, context.root),
+                            Mount(context.staging, "/work/out"),
                             *context.config.distribution.package_manager(context.config).mounts(context),
-                            "--chdir", "/work/src",
                         ],
+                        options=["--dir", "/work/src", "--chdir", "/work/src"],
                         scripts=hd,
                     ) + (chroot if script.suffix == ".chroot" else []),
                 )
@@ -742,15 +748,16 @@ def run_finalize_scripts(context: Context) -> None:
                     stdin=sys.stdin,
                     sandbox=context.sandbox(
                         network=context.config.with_network,
-                        options=sources + [
-                            "--ro-bind", script, "/work/finalize",
-                            "--ro-bind", json, "/work/config.json",
-                            "--ro-bind", cd, "/work/scripts",
-                            "--bind", context.root, context.root,
-                            "--bind", context.staging, "/work/out",
+                        mounts=[
+                            *sources,
+                            Mount(script, "/work/finalize", ro=True),
+                            Mount(json, "/work/config.json", ro=True),
+                            Mount(cd, "/work/scripts", ro=True),
+                            Mount(context.root, context.root),
+                            Mount(context.staging, "/work/out"),
                             *context.config.distribution.package_manager(context.config).mounts(context),
-                            "--chdir", "/work/src",
                         ],
+                        options=["--dir", "/work/src", "--chdir", "/work/src"],
                         scripts=hd,
                     ) + (chroot if script.suffix == ".chroot" else []),
                 )
@@ -767,7 +774,7 @@ def certificate_common_name(context: Context, certificate: Path) -> str:
             "-in", certificate,
         ],
         stdout=subprocess.PIPE,
-        sandbox=context.sandbox(options=["--ro-bind", certificate, certificate]),
+        sandbox=context.sandbox(mounts=[Mount(certificate, certificate, ro=True)]),
     ).stdout
 
     for line in output.splitlines():
@@ -811,9 +818,9 @@ def pesign_prepare(context: Context) -> None:
             ],
             stdout=f,
             sandbox=context.sandbox(
-                options=[
-                    "--ro-bind", context.config.secure_boot_key, context.config.secure_boot_key,
-                    "--ro-bind", context.config.secure_boot_certificate, context.config.secure_boot_certificate,
+                mounts=[
+                    Mount(context.config.secure_boot_key, context.config.secure_boot_key, ro=True),
+                    Mount(context.config.secure_boot_certificate, context.config.secure_boot_certificate, ro=True),
                 ],
             ),
         )
@@ -829,9 +836,9 @@ def pesign_prepare(context: Context) -> None:
             "-d", context.workspace / "pesign",
         ],
         sandbox=context.sandbox(
-            options=[
-                "--ro-bind", context.workspace / "secure-boot.p12", context.workspace / "secure-boot.p12",
-                "--bind", context.workspace / "pesign", context.workspace / "pesign",
+            mounts=[
+                Mount(context.workspace / "secure-boot.p12", context.workspace / "secure-boot.p12", ro=True),
+                Mount(context.workspace / "pesign", context.workspace / "pesign"),
             ],
         ),
     )
@@ -869,20 +876,20 @@ def sign_efi_binary(context: Context, input: Path, output: Path) -> Path:
                 "--cert", context.config.secure_boot_certificate,
                 "--output", "/dev/stdout",
             ]
-            options: list[PathString] = [
-                "--ro-bind", context.config.secure_boot_certificate, context.config.secure_boot_certificate,
-                "--ro-bind", input, input,
+            mounts = [
+                Mount(context.config.secure_boot_certificate, context.config.secure_boot_certificate, ro=True),
+                Mount(input, input, ro=True),
             ]
             if context.config.secure_boot_key_source.type == KeySource.Type.engine:
                 cmd += ["--engine", context.config.secure_boot_key_source.source]
             if context.config.secure_boot_key.exists():
-                options += ["--ro-bind", context.config.secure_boot_key, context.config.secure_boot_key]
+                mounts += [Mount(context.config.secure_boot_key, context.config.secure_boot_key, ro=True)]
             cmd += [input]
             run(
                 cmd,
                 stdout=f,
                 sandbox=context.sandbox(
-                    options=options,
+                    mounts=mounts,
                     devices=context.config.secure_boot_key_source.type != KeySource.Type.file,
                 )
             )
@@ -908,9 +915,9 @@ def sign_efi_binary(context: Context, input: Path, output: Path) -> Path:
                 ],
                 stdout=f,
                 sandbox=context.sandbox(
-                    options=[
-                        "--ro-bind", context.workspace / "pesign", context.workspace / "pesign",
-                        "--ro-bind", input, input,
+                    mounts=[
+                        Mount(context.workspace / "pesign", context.workspace / "pesign", ro=True),
+                        Mount(input, input, ro=True),
                     ]
                 ),
             )
@@ -955,7 +962,7 @@ def install_systemd_boot(context: Context) -> None:
         run(
             ["bootctl", "install", "--root", context.root, "--all-architectures", "--no-variables"],
             env={"SYSTEMD_ESP_PATH": "/efi", "SYSTEMD_XBOOTLDR_PATH": "/boot"},
-            sandbox=context.sandbox(options=["--bind", context.root, context.root]),
+            sandbox=context.sandbox(mounts=[Mount(context.root, context.root)]),
         )
 
         if context.config.shim_bootloader != ShimBootloader.none:
@@ -984,10 +991,12 @@ def install_systemd_boot(context: Context) -> None:
                     ],
                     stdout=f,
                     sandbox=context.sandbox(
-                        options=[
-                            "--ro-bind",
-                            context.config.secure_boot_certificate,
-                            context.config.secure_boot_certificate,
+                        mounts=[
+                            Mount(
+                                context.config.secure_boot_certificate,
+                                context.config.secure_boot_certificate,
+                                ro=True
+                            ),
                         ],
                     ),
                 )
@@ -1003,7 +1012,7 @@ def install_systemd_boot(context: Context) -> None:
                     ],
                     stdout=f,
                     sandbox=context.sandbox(
-                        options=["--ro-bind", context.workspace / "mkosi.der", context.workspace / "mkosi.der"]
+                        mounts=[Mount(context.workspace / "mkosi.der", context.workspace / "mkosi.der", ro=True)]
                     ),
                 )
 
@@ -1018,22 +1027,24 @@ def install_systemd_boot(context: Context) -> None:
                         "--cert", context.config.secure_boot_certificate,
                         "--output", "/dev/stdout",
                     ]
-                    options: list[PathString] = [
-                        "--ro-bind",
-                        context.config.secure_boot_certificate,
-                        context.config.secure_boot_certificate,
-                        "--ro-bind", context.workspace / "mkosi.esl", context.workspace / "mkosi.esl",
+                    mounts = [
+                        Mount(
+                            context.config.secure_boot_certificate,
+                            context.config.secure_boot_certificate,
+                            ro=True
+                        ),
+                        Mount(context.workspace / "mkosi.esl", context.workspace / "mkosi.esl", ro=True),
                     ]
                     if context.config.secure_boot_key_source.type == KeySource.Type.engine:
                         cmd += ["--engine", context.config.secure_boot_key_source.source]
                     if context.config.secure_boot_key.exists():
-                        options += ["--ro-bind", context.config.secure_boot_key, context.config.secure_boot_key]
+                        mounts += [Mount(context.config.secure_boot_key, context.config.secure_boot_key, ro=True)]
                     cmd += [db, context.workspace / "mkosi.esl"]
                     run(
                         cmd,
                         stdout=f,
                         sandbox=context.sandbox(
-                            options=options,
+                            mounts=mounts,
                             devices=context.config.secure_boot_key_source.type != KeySource.Type.file,
                         ),
                     )
@@ -1296,10 +1307,10 @@ def grub_mkimage(
                 *modules,
             ],
             sandbox=context.sandbox(
-                options=[
-                    "--bind", context.root, context.root,
-                    "--ro-bind", earlyconfig.name, earlyconfig.name,
-                    *(["--ro-bind", str(sbat), str(sbat)] if sbat else []),
+                mounts=[
+                    Mount(context.root, context.root),
+                    Mount(earlyconfig.name, earlyconfig.name, ro=True),
+                    *([Mount(str(sbat), str(sbat), ro=True)] if sbat else []),
                 ],
             ),
         )
@@ -1405,10 +1416,10 @@ def grub_bios_setup(context: Context, partitions: Sequence[Partition]) -> None:
                 context.staging / context.config.output_with_format,
             ],
             sandbox=context.sandbox(
-                options=[
-                    "--bind", context.root, context.root,
-                    "--bind", context.staging, context.staging,
-                    "--bind", mountinfo.name, mountinfo.name,
+                mounts=[
+                    Mount(context.root, context.root),
+                    Mount(context.staging, context.staging),
+                    Mount(mountinfo.name, mountinfo.name),
                 ],
             ),
         )
@@ -1448,7 +1459,7 @@ def install_tree(
             sandbox=config.sandbox(
                 devices=True,
                 network=True,
-                options=["--ro-bind", src, src, "--bind", t.parent, t.parent],
+                mounts=[Mount(src, src, ro=True), Mount(t.parent, t.parent)],
             ),
         )
     else:
@@ -1869,7 +1880,7 @@ def extract_pe_section(context: Context, binary: Path, section: str, output: Pat
             [python_binary(context.config)],
             input=pefile,
             stdout=f,
-            sandbox=context.sandbox(options=["--ro-bind", binary, binary])
+            sandbox=context.sandbox(mounts=[Mount(binary, binary, ro=True)])
         )
 
     return output
@@ -1911,11 +1922,11 @@ def build_uki(
         "--uname", kver,
     ]
 
-    options: list[PathString] = [
-        "--bind", output.parent, output.parent,
-        "--ro-bind", context.workspace / "cmdline", context.workspace / "cmdline",
-        "--ro-bind", context.root / "usr/lib/os-release", context.root / "usr/lib/os-release",
-        "--ro-bind", stub, stub,
+    mounts = [
+        Mount(output.parent, output.parent),
+        Mount(context.workspace / "cmdline", context.workspace / "cmdline", ro=True),
+        Mount(context.root / "usr/lib/os-release", context.root / "usr/lib/os-release", ro=True),
+        Mount(stub, stub, ro=True),
     ]
 
     if context.config.secure_boot:
@@ -1932,13 +1943,13 @@ def build_uki(
                 "--secureboot-certificate",
                 context.config.secure_boot_certificate,
             ]
-            options += [
-                "--ro-bind", context.config.secure_boot_certificate, context.config.secure_boot_certificate,
+            mounts += [
+                Mount(context.config.secure_boot_certificate, context.config.secure_boot_certificate, ro=True),
             ]
             if context.config.secure_boot_key_source.type == KeySource.Type.engine:
                 cmd += ["--signing-engine", context.config.secure_boot_key_source.source]
             if context.config.secure_boot_key.exists():
-                options += ["--ro-bind", context.config.secure_boot_key, context.config.secure_boot_key]
+                mounts += [Mount(context.config.secure_boot_key, context.config.secure_boot_key, ro=True)]
         else:
             pesign_prepare(context)
             cmd += [
@@ -1948,7 +1959,7 @@ def build_uki(
                 "--secureboot-certificate-name",
                 certificate_common_name(context, context.config.secure_boot_certificate),
             ]
-            options += ["--ro-bind", context.workspace / "pesign", context.workspace / "pesign"]
+            mounts += [Mount(context.workspace / "pesign", context.workspace / "pesign", ro=True)]
 
         if want_signed_pcrs(context.config):
             cmd += [
@@ -1956,28 +1967,28 @@ def build_uki(
                 "--pcr-banks", "sha1,sha256",
             ]
             if context.config.secure_boot_key.exists():
-                options += ["--ro-bind", context.config.secure_boot_key, context.config.secure_boot_key]
+                mounts += [Mount(context.config.secure_boot_key, context.config.secure_boot_key)]
             if context.config.secure_boot_key_source.type == KeySource.Type.engine:
                 cmd += [
                     "--signing-engine", context.config.secure_boot_key_source.source,
                     "--pcr-public-key", context.config.secure_boot_certificate,
                 ]
-                options += [
-                    "--ro-bind", context.config.secure_boot_certificate, context.config.secure_boot_certificate,
+                mounts += [
+                    Mount(context.config.secure_boot_certificate, context.config.secure_boot_certificate, ro=True),
                 ]
 
     cmd += ["build", "--linux", kimg]
-    options += ["--ro-bind", kimg, kimg]
+    mounts += [Mount(kimg, kimg, ro=True)]
 
     for initrd in initrds:
         cmd += ["--initrd", initrd]
-        options += ["--ro-bind", initrd, initrd]
+        mounts += [Mount(initrd, initrd, ro=True)]
 
     with complete_step(f"Generating unified kernel image for kernel version {kver}"):
         run(
             cmd,
             sandbox=context.sandbox(
-                options=options,
+                mounts=mounts,
                 devices=context.config.secure_boot_key_source.type != KeySource.Type.file,
             ),
         )
@@ -2036,7 +2047,7 @@ def find_entry_token(context: Context) -> str:
         return context.config.image_id or context.config.distribution.name
 
     output = json.loads(run(["kernel-install", "--root", context.root, "--json=pretty", "inspect"],
-                            sandbox=context.sandbox(options=["--ro-bind", context.root, context.root]),
+                            sandbox=context.sandbox(mounts=[Mount(context.root, context.root, ro=True)]),
                             stdout=subprocess.PIPE,
                             env={"SYSTEMD_ESP_PATH": "/efi", "SYSTEMD_XBOOTLDR_PATH": "/boot"}).stdout)
     logging.debug(json.dumps(output, indent=4))
@@ -2431,18 +2442,20 @@ def calculate_signature(context: Context) -> None:
     if sys.stderr.isatty():
         env |= dict(GPGTTY=os.ttyname(sys.stderr.fileno()))
 
-    options: list[PathString] = ["--perms", "755", "--dir", home, "--bind", home, home]
+    options: list[PathString] = ["--perms", "755", "--dir", home]
+    mounts = [Mount(home, home)]
 
     # gpg can communicate with smartcard readers via this socket so bind mount it in if it exists.
     if (p := Path("/run/pcscd/pcscd.comm")).exists():
-        options += ["--perms", "755", "--dir", p.parent, "--bind", p, p]
+        options += ["--perms", "755", "--dir", p.parent]
+        mounts += [Mount(p, p)]
 
     with (
         complete_step("Signing SHA256SUMS…"),
         open(context.staging / context.config.output_checksum, "rb") as i,
         open(context.staging / context.config.output_signature, "wb") as o,
     ):
-        run(cmdline, env=env, stdin=i, stdout=o, sandbox=context.sandbox(options=options))
+        run(cmdline, env=env, stdin=i, stdout=o, sandbox=context.sandbox(mounts=mounts, options=options))
 
 
 def dir_size(path: Union[Path, os.DirEntry[str]]) -> int:
@@ -2736,7 +2749,7 @@ def run_depmod(context: Context, *, cache: bool = False) -> None:
 
         with complete_step(f"Running depmod for {kver}"):
             run(["depmod", "--all", "--basedir", context.root, kver],
-                sandbox=context.sandbox(options=["--bind", context.root, context.root]))
+                sandbox=context.sandbox(mounts=[Mount(context.root, context.root)]))
 
 
 def run_sysusers(context: Context) -> None:
@@ -2746,7 +2759,7 @@ def run_sysusers(context: Context) -> None:
 
     with complete_step("Generating system users"):
         run(["systemd-sysusers", "--root", context.root],
-            sandbox=context.sandbox(options=["--bind", context.root, context.root]))
+            sandbox=context.sandbox(mounts=[Mount(context.root, context.root)]))
 
 
 def run_tmpfiles(context: Context) -> None:
@@ -2766,8 +2779,8 @@ def run_tmpfiles(context: Context) -> None:
         ]
 
         sandbox = context.sandbox(
-            options=[
-                "--bind", context.root, context.root,
+            mounts=[
+                Mount(context.root, context.root),
                 # systemd uses acl.h to parse ACLs in tmpfiles snippets which uses the host's passwd so we have to
                 # mount the image's passwd over it to make ACL parsing work.
                 *finalize_passwd_mounts(context.root)
@@ -2794,9 +2807,9 @@ def run_preset(context: Context) -> None:
 
     with complete_step("Applying presets…"):
         run(["systemctl", "--root", context.root, "preset-all"],
-            sandbox=context.sandbox(options=["--bind", context.root, context.root]))
+            sandbox=context.sandbox(mounts=[Mount(context.root, context.root)]))
         run(["systemctl", "--root", context.root, "--global", "preset-all"],
-            sandbox=context.sandbox(options=["--bind", context.root, context.root]))
+            sandbox=context.sandbox(mounts=[Mount(context.root, context.root)]))
 
 
 def run_hwdb(context: Context) -> None:
@@ -2809,7 +2822,7 @@ def run_hwdb(context: Context) -> None:
 
     with complete_step("Generating hardware database"):
         run(["systemd-hwdb", "--root", context.root, "--usr", "--strict", "update"],
-            sandbox=context.sandbox(options=["--bind", context.root, context.root]))
+            sandbox=context.sandbox(mounts=[Mount(context.root, context.root)]))
 
     # Remove any existing hwdb in /etc in favor of the one we just put in /usr.
     (context.root / "etc/udev/hwdb.bin").unlink(missing_ok=True)
@@ -2856,7 +2869,7 @@ def run_firstboot(context: Context) -> None:
 
     with complete_step("Applying first boot settings"):
         run(["systemd-firstboot", "--root", context.root, "--force", *options],
-            sandbox=context.sandbox(options=["--bind", context.root, context.root]))
+            sandbox=context.sandbox(mounts=[Mount(context.root, context.root)]))
 
         # Initrds generally don't ship with only /usr so there's not much point in putting the credentials in
         # /usr/lib/credstore.
@@ -2877,7 +2890,7 @@ def run_selinux_relabel(context: Context) -> None:
 
     with complete_step(f"Relabeling files using {policy} policy"):
         run(["setfiles", "-mFr", context.root, "-c", binpolicy, fc, context.root],
-            sandbox=context.sandbox(options=["--bind", context.root, context.root]),
+            sandbox=context.sandbox(mounts=[Mount(context.root, context.root)]),
             check=context.config.selinux_relabel == ConfigFeature.enabled)
 
 
@@ -3018,27 +3031,27 @@ def make_image(
         "--seed", str(context.config.seed),
         context.staging / context.config.output_with_format,
     ]
-    options: list[PathString] = ["--bind", context.staging, context.staging]
+    mounts = [Mount(context.staging, context.staging)]
 
     if root:
         cmdline += ["--root", root]
-        options += ["--bind", root, root]
+        mounts += [Mount(root, root)]
     if not context.config.architecture.is_native():
         cmdline += ["--architecture", str(context.config.architecture)]
     if not (context.staging / context.config.output_with_format).exists():
         cmdline += ["--empty=create"]
     if context.config.passphrase:
         cmdline += ["--key-file", context.config.passphrase]
-        options += ["--ro-bind", context.config.passphrase, context.config.passphrase]
+        mounts += [Mount(context.config.passphrase, context.config.passphrase, ro=True)]
     if context.config.verity_key:
         cmdline += ["--private-key", context.config.verity_key]
         if context.config.verity_key_source.type != KeySource.Type.file:
             cmdline += ["--private-key-source", str(context.config.verity_key_source)]
         if context.config.verity_key.exists():
-            options += ["--ro-bind", context.config.verity_key, context.config.verity_key]
+            mounts += [Mount(context.config.verity_key, context.config.verity_key, ro=True)]
     if context.config.verity_certificate:
         cmdline += ["--certificate", context.config.verity_certificate]
-        options += ["--ro-bind", context.config.verity_certificate, context.config.verity_certificate]
+        mounts += [Mount(context.config.verity_certificate, context.config.verity_certificate, ro=True)]
     if skip:
         cmdline += ["--defer-partitions", ",".join(skip)]
     if split:
@@ -3053,7 +3066,7 @@ def make_image(
 
     for d in definitions:
         cmdline += ["--definitions", d]
-        options += ["--ro-bind", d, d]
+        mounts += [Mount(d, d, ro=True)]
 
     with complete_step(msg):
         output = json.loads(
@@ -3066,7 +3079,7 @@ def make_image(
                         not context.config.repart_offline or
                         context.config.verity_key_source.type != KeySource.Type.file
                     ),
-                    options=options,
+                    mounts=mounts,
                 ),
             ).stdout
         )
@@ -3207,25 +3220,25 @@ def make_extension_image(context: Context, output: Path) -> None:
         "--size=auto",
         output,
     ]
-    options: list[PathString] = [
-        "--bind", output.parent, output.parent,
-        "--ro-bind", context.root, context.root,
+    mounts = [
+        Mount(output.parent, output.parent),
+        Mount(context.root, context.root, ro=True),
     ]
 
     if not context.config.architecture.is_native():
         cmdline += ["--architecture", str(context.config.architecture)]
     if context.config.passphrase:
         cmdline += ["--key-file", context.config.passphrase]
-        options += ["--ro-bind", context.config.passphrase, context.config.passphrase]
+        mounts += [Mount(context.config.passphrase, context.config.passphrase, ro=True)]
     if context.config.verity_key:
         cmdline += ["--private-key", context.config.verity_key]
         if context.config.verity_key_source.type != KeySource.Type.file:
             cmdline += ["--private-key-source", str(context.config.verity_key_source)]
         if context.config.verity_key.exists():
-            options += ["--ro-bind", context.config.verity_key, context.config.verity_key]
+            mounts += [Mount(context.config.verity_key, context.config.verity_key, ro=True)]
     if context.config.verity_certificate:
         cmdline += ["--certificate", context.config.verity_certificate]
-        options += ["--ro-bind", context.config.verity_certificate, context.config.verity_certificate]
+        mounts += [Mount(context.config.verity_certificate, context.config.verity_certificate, ro=True)]
     if context.config.sector_size:
         cmdline += ["--sector-size", str(context.config.sector_size)]
 
@@ -3237,7 +3250,7 @@ def make_extension_image(context: Context, output: Path) -> None:
 
     with complete_step(f"Building {context.config.output_format} extension image"):
         r = context.resources / f"repart/definitions/{context.config.output_format}.repart.d"
-        options += ["--ro-bind", r, r]
+        mounts += [Mount(r, r, ro=True)]
         run(
             cmdline + ["--definitions", r],
             env=env,
@@ -3246,7 +3259,7 @@ def make_extension_image(context: Context, output: Path) -> None:
                     not context.config.repart_offline or
                     context.config.verity_key_source.type != KeySource.Type.file
                 ),
-                options=options,
+                mounts=mounts,
             ),
         )
 
@@ -3360,14 +3373,14 @@ def copy_repository_metadata(context: Context) -> None:
 
                 # cp doesn't support excluding directories but we can imitate it by bind mounting an empty directory
                 # over the directories we want to exclude.
-                exclude = flatten(["--ro-bind", tmp, os.fspath(p)] for p in caches)
+                exclude = [Mount(tmp, p, ro=True) for p in caches]
 
                 dst = context.package_cache_dir / d / subdir
                 with umask(~0o755):
                     dst.mkdir(parents=True, exist_ok=True)
 
-                def sandbox(*, options: Sequence[PathString] = ()) -> list[PathString]:
-                    return context.sandbox(options=[*options, *exclude])
+                def sandbox(*, mounts: Sequence[Mount] = ()) -> list[PathString]:
+                    return context.sandbox(mounts=[*mounts, *exclude])
 
                 copy_tree(
                     src, dst,
@@ -3518,7 +3531,7 @@ def setfacl(config: Config, root: Path, uid: int, allow: bool) -> None:
         ],
         # Supply files via stdin so we don't clutter --debug run output too much
         input="\n".join([str(root), *(os.fspath(p) for p in root.rglob("*") if p.is_dir())]),
-        sandbox=config.sandbox(options=["--bind", root, root]),
+        sandbox=config.sandbox(mounts=[Mount(root, root)]),
     )
 
 
@@ -3530,7 +3543,7 @@ def acl_maybe_toggle(config: Config, root: Path, uid: int, *, always: bool) -> I
 
     # getfacl complains about absolute paths so make sure we pass a relative one.
     if root.exists():
-        sandbox = config.sandbox(options=["--bind", root, root, "--chdir", root])
+        sandbox = config.sandbox(mounts=[Mount(root, root)], options=["--chdir", root])
         has_acl = f"user:{uid}:rwx" in run(["getfacl", "-n", "."], sandbox=sandbox, stdout=subprocess.PIPE).stdout
 
         if not has_acl and not always:
@@ -3648,7 +3661,7 @@ def run_shell(args: Args, config: Config) -> None:
                 ],
                 stdin=sys.stdin,
                 env=config.environment,
-                sandbox=config.sandbox(network=True, devices=True, options=["--bind", fname, fname]),
+                sandbox=config.sandbox(network=True, devices=True, mounts=[Mount(fname, fname)]),
             )
 
         if config.output_format == OutputFormat.directory:
index 6ffa3930d60969f2bb79b529c73710bf4860c20f..32a614c049406bb2ddc9d146abd5e433b2b7c2f3 100644 (file)
@@ -7,7 +7,7 @@ from typing import Optional
 
 from mkosi.log import log_step
 from mkosi.run import find_binary, run
-from mkosi.sandbox import SandboxProtocol, finalize_passwd_mounts, nosandbox
+from mkosi.sandbox import Mount, SandboxProtocol, finalize_passwd_mounts, nosandbox
 from mkosi.util import umask
 
 
@@ -61,7 +61,7 @@ def make_tar(src: Path, dst: Path, *, tools: Path = Path("/"), sandbox: SandboxP
             ],
             stdout=f,
             # Make sure tar uses user/group information from the root directory instead of the host.
-            sandbox=sandbox(options=["--ro-bind", src, src, *finalize_passwd_mounts(src)]),
+            sandbox=sandbox(mounts=[Mount(src, src, ro=True), *finalize_passwd_mounts(src)]),
         )
 
 
@@ -100,7 +100,7 @@ def extract_tar(
             stdin=f,
             sandbox=sandbox(
                 # Make sure tar uses user/group information from the root directory instead of the host.
-                options=["--ro-bind", src, src, "--bind", dst, dst, *finalize_passwd_mounts(dst)]
+                mounts=[Mount(src, src, ro=True), Mount(dst, dst), *finalize_passwd_mounts(dst)]
             ),
         )
 
@@ -132,5 +132,5 @@ def make_cpio(
             ],
             input="\0".join(os.fspath(f.relative_to(src)) for f in files),
             stdout=f,
-            sandbox=sandbox(options=["--ro-bind", src, src, *finalize_passwd_mounts(src)]),
+            sandbox=sandbox(mounts=[Mount(src, src, ro=True), *finalize_passwd_mounts(src)]),
         )
index b7ff56309ed7f0eb31d85d8f14393934ea2e1a9f..26030e68aa77df1bfcacf9c47828142916880b53 100644 (file)
@@ -33,7 +33,7 @@ from mkosi.distributions import Distribution, detect_distribution
 from mkosi.log import ARG_DEBUG, ARG_DEBUG_SHELL, Style, die
 from mkosi.pager import page
 from mkosi.run import find_binary, run
-from mkosi.sandbox import sandbox_cmd
+from mkosi.sandbox import Mount, sandbox_cmd
 from mkosi.types import PathString, SupportsRead
 from mkosi.user import INVOKING_USER
 from mkosi.util import (
@@ -1551,13 +1551,13 @@ class Config:
         devices: bool = False,
         relaxed: bool = False,
         scripts: Optional[Path] = None,
+        mounts: Sequence[Mount] = (),
         options: Sequence[PathString] = (),
     ) -> list[PathString]:
-        mounts: list[PathString] = (
-            flatten(("--ro-bind", d, d) for d in self.extra_search_paths)
-            if not relaxed and not self.tools_tree
-            else []
-        )
+        mounts = [
+            *[Mount(d, d, ro=True) for d in self.extra_search_paths if not relaxed and not self.tools_tree],
+            *mounts,
+        ]
 
         return sandbox_cmd(
             network=network,
@@ -1565,7 +1565,8 @@ class Config:
             relaxed=relaxed,
             scripts=scripts,
             tools=self.tools(),
-            options=[*mounts, *options],
+            mounts=mounts,
+            options=options,
         )
 
 
@@ -3895,7 +3896,7 @@ def want_selinux_relabel(config: Config, root: Path, fatal: bool = True) -> Opti
         return None
 
     policy = run(["sh", "-c", f". {selinux} && echo $SELINUXTYPE"],
-                 sandbox=config.sandbox(options=["--ro-bind", selinux, selinux]),
+                 sandbox=config.sandbox(mounts=[Mount(selinux, selinux, ro=True)]),
                  stdout=subprocess.PIPE).stdout.strip()
     if not policy:
         if fatal and config.selinux_relabel == ConfigFeature.enabled:
index c0b85e4853a74a1788f44afa12ece14965a2e690..6b630fbfaafbaa1118e7e2b7a84be9ebac1a63b1 100644 (file)
@@ -1,14 +1,14 @@
 # SPDX-License-Identifier: LGPL-2.1+
 
-import os
 from collections.abc import Sequence
 from pathlib import Path
 from typing import Optional
 
 from mkosi.config import Args, Config
+from mkosi.sandbox import Mount
 from mkosi.tree import make_tree
 from mkosi.types import PathString
-from mkosi.util import flatten, umask
+from mkosi.util import umask
 
 
 class Context:
@@ -73,26 +73,30 @@ class Context:
         network: bool = False,
         devices: bool = False,
         scripts: Optional[Path] = None,
+        mounts: Sequence[Mount] = (),
         options: Sequence[PathString] = (),
     ) -> list[PathString]:
         return self.config.sandbox(
             network=network,
             devices=devices,
             scripts=scripts,
-            options=[
-                "--uid", "0",
-                "--gid", "0",
-                "--cap-add", "ALL",
+            mounts=[
                 # These mounts are writable so bubblewrap can create extra directories or symlinks inside of it as
                 # needed. This isn't a problem as the package manager directory is created by mkosi and thrown away
                 # when the build finishes.
-                *flatten(
-                    ["--bind", os.fspath(self.pkgmngr / "etc" / p.name), f"/etc/{p.name}"]
+                *[
+                    Mount(self.pkgmngr / "etc" / p.name, f"/etc/{p.name}")
                     for p in (self.pkgmngr / "etc").iterdir()
-                ),
+                ],
+                *mounts,
+                Mount(self.pkgmngr / "var/log", "/var/log"),
+                *([Mount(p, p, ro=True)] if (p := self.pkgmngr / "usr").exists() else []),
+            ],
+            options=[
+                "--uid", "0",
+                "--gid", "0",
+                "--cap-add", "ALL",
                 *options,
-                "--bind", self.pkgmngr / "var/log", "/var/log",
-                *(["--ro-bind", os.fspath(p), os.fspath(p)] if (p := self.pkgmngr / "usr").exists() else []),
             ],
         ) + (
             [
index 568e8d2587e399d81ad59796f264d157edd2fc99..4c16ca450f783b258764454ae7a9d87f1ad23533 100644 (file)
@@ -13,6 +13,7 @@ from mkosi.installer import PackageManager
 from mkosi.installer.apt import Apt
 from mkosi.log import die
 from mkosi.run import run
+from mkosi.sandbox import Mount
 from mkosi.util import listify, umask
 
 
@@ -153,7 +154,7 @@ class Installer(DistributionInstaller):
                     f"-oDPkg::Pre-Install-Pkgs::=cat >{f.name}",
                     "?essential", "?exact-name(usr-is-merged)",
                 ],
-                mounts=("--bind", f.name, f.name),
+                mounts=[Mount(f.name, f.name)],
             )
 
             essential = f.read().strip().splitlines()
index b37bc72b2d69233b52ae1027d7c2a8fb9c3a276b..8451e22308239a1dd9a16fd1c40bb35907527328 100644 (file)
@@ -14,7 +14,7 @@ from mkosi.installer.rpm import RpmRepository, find_rpm_gpgkey, setup_rpm
 from mkosi.installer.zypper import Zypper
 from mkosi.log import die
 from mkosi.run import find_binary, run
-from mkosi.sandbox import finalize_crypto_mounts
+from mkosi.sandbox import Mount, finalize_crypto_mounts
 from mkosi.util import listify, sort_packages
 
 
@@ -168,7 +168,7 @@ def fetch_gpgurls(context: Context, repourl: str) -> tuple[str, ...]:
             ],
             sandbox=context.sandbox(
                 network=True,
-                options=["--bind", d, d, *finalize_crypto_mounts(context.config.tools())],
+                mounts=[Mount(d, d), *finalize_crypto_mounts(context.config.tools())],
             ),
         )
         xml = (Path(d) / "repomd.xml").read_text()
index eb2f12bfdd24701079142a36f5aa3303db9e5401..04340b4275095b911659548f041d81de5856749b 100644 (file)
@@ -1,15 +1,14 @@
 # SPDX-License-Identifier: LGPL-2.1+
 
-import os
 from pathlib import Path
 
 from mkosi.config import Config, ConfigFeature, OutputFormat
 from mkosi.context import Context
 from mkosi.run import find_binary
-from mkosi.sandbox import finalize_crypto_mounts
+from mkosi.sandbox import Mount, finalize_crypto_mounts
 from mkosi.tree import copy_tree, rmtree
 from mkosi.types import PathString
-from mkosi.util import flatten, startswith
+from mkosi.util import startswith
 
 
 class PackageManager:
@@ -30,20 +29,20 @@ class PackageManager:
         return {}
 
     @classmethod
-    def mounts(cls, context: Context) -> list[PathString]:
-        mounts: list[PathString] = [
+    def mounts(cls, context: Context) -> list[Mount]:
+        mounts = [
             *finalize_crypto_mounts(tools=context.config.tools()),
-            "--bind", context.packages, "/work/packages",
+            Mount(context.packages, "/work/packages"),
         ]
 
         if context.config.local_mirror and (mirror := startswith(context.config.local_mirror, "file://")):
-            mounts += ["--ro-bind", mirror, mirror]
+            mounts += [Mount(mirror, mirror, ro=True)]
 
         subdir = context.config.distribution.package_manager(context.config).subdir(context.config)
 
         for d in ("cache", "lib"):
             src = context.package_cache_dir / d / subdir
-            mounts += ["--bind", src, Path("/var") / d / subdir]
+            mounts += [Mount(src, Path("/var") / d / subdir)]
 
             # If we're not operating on the configured package cache directory, we're operating on a snapshot of the
             # repository metadata in the image root directory. To make sure any downloaded packages are still cached in
@@ -51,15 +50,14 @@ class PackageManager:
             # configured package cache directory.
             if d == "cache" and context.package_cache_dir != context.config.package_cache_dir_or_default():
                 caches = context.config.distribution.package_manager(context.config).cache_subdirs(src)
-                mounts += flatten(
-                    [
-                        "--bind",
-                        os.fspath(context.config.package_cache_dir_or_default() / d / subdir / p.relative_to(src)),
+                mounts += [
+                    Mount(
+                        context.config.package_cache_dir_or_default() / d / subdir / p.relative_to(src),
                         Path("/var") / d / subdir / p.relative_to(src),
-                    ]
+                    )
                     for p in caches
                     if (context.config.package_cache_dir_or_default() / d / subdir / p.relative_to(src)).exists()
-                )
+                ]
 
         return mounts
 
index 440d34834278304a928cb2bb573f574940b0a0c9..7cfa84cf279b098a03a722eee7aa93ee8d78dbde 100644 (file)
@@ -11,7 +11,7 @@ from mkosi.installer import PackageManager
 from mkosi.log import die
 from mkosi.mounts import finalize_source_mounts
 from mkosi.run import find_binary, run
-from mkosi.sandbox import apivfs_cmd
+from mkosi.sandbox import Mount, apivfs_cmd
 from mkosi.types import _FILE, CompletedProcess, PathString
 from mkosi.util import umask
 
@@ -173,7 +173,7 @@ class Apt(PackageManager):
         arguments: Sequence[str] = (),
         *,
         apivfs: bool = False,
-        mounts: Sequence[PathString] = (),
+        mounts: Sequence[Mount] = (),
         stdout: _FILE = None,
     ) -> CompletedProcess:
         with finalize_source_mounts(
@@ -185,13 +185,8 @@ class Apt(PackageManager):
                 sandbox=(
                     context.sandbox(
                         network=True,
-                        options=[
-                            "--bind", context.root, context.root,
-                            *cls.mounts(context),
-                            *sources,
-                            *mounts,
-                            "--chdir", "/work/src",
-                        ],
+                        mounts=[Mount(context.root, context.root), *cls.mounts(context), *sources, *mounts],
+                        options=["--dir", "/work/src", "--chdir", "/work/src"],
                     ) + (apivfs_cmd(context.root) if apivfs else [])
                 ),
                 env=context.config.environment,
@@ -209,10 +204,8 @@ class Apt(PackageManager):
                 ["dpkg-scanpackages", "."],
                 stdout=f,
                 sandbox=context.sandbox(
-                    options=[
-                        "--ro-bind", context.packages, context.packages,
-                        "--chdir", context.packages,
-                    ],
+                    mounts=[Mount(context.packages, context.packages, ro=True)],
+                    options=["--chdir", context.packages],
                 ),
             )
 
index e74fd48c29a627bee47063cbcc72b7411da09b2b..7b6fc0ede440462882e609864f9713b99602cdd1 100644 (file)
@@ -11,7 +11,7 @@ from mkosi.installer.rpm import RpmRepository, rpm_cmd
 from mkosi.log import ARG_DEBUG
 from mkosi.mounts import finalize_source_mounts
 from mkosi.run import find_binary, run
-from mkosi.sandbox import apivfs_cmd
+from mkosi.sandbox import Mount, apivfs_cmd
 from mkosi.types import _FILE, CompletedProcess, PathString
 
 
@@ -170,12 +170,8 @@ class Dnf(PackageManager):
                     sandbox=(
                         context.sandbox(
                             network=True,
-                            options=[
-                                "--bind", context.root, context.root,
-                                *cls.mounts(context),
-                                *sources,
-                                "--chdir", "/work/src",
-                            ],
+                            mounts=[Mount(context.root, context.root), *cls.mounts(context), *sources],
+                            options=["--dir", "/work/src", "--chdir", "/work/src"],
                         ) + (apivfs_cmd(context.root) if apivfs else [])
                     ),
                     env=context.config.environment,
@@ -204,7 +200,7 @@ class Dnf(PackageManager):
     @classmethod
     def createrepo(cls, context: Context) -> None:
         run(["createrepo_c", context.packages],
-            sandbox=context.sandbox(options=["--bind", context.packages, context.packages]))
+            sandbox=context.sandbox(mounts=[Mount(context.packages, context.packages)]))
 
         (context.pkgmngr / "etc/yum.repos.d/mkosi-local.repo").write_text(
             textwrap.dedent(
index b1b6282c31f179503e97e8b4ac2d1d124049f1ff..e5faeff220844987384812ed1377a5f8322385c4 100644 (file)
@@ -11,7 +11,7 @@ from mkosi.context import Context
 from mkosi.installer import PackageManager
 from mkosi.mounts import finalize_source_mounts
 from mkosi.run import run
-from mkosi.sandbox import apivfs_cmd
+from mkosi.sandbox import Mount, apivfs_cmd
 from mkosi.types import _FILE, CompletedProcess, PathString
 from mkosi.util import umask
 from mkosi.versioncomp import GenericVersion
@@ -45,25 +45,25 @@ class Pacman(PackageManager):
         }
 
     @classmethod
-    def mounts(cls, context: Context) -> list[PathString]:
-        mounts: list[PathString] = [
+    def mounts(cls, context: Context) -> list[Mount]:
+        mounts = [
             *super().mounts(context),
             # pacman writes downloaded packages to the first writable cache directory. We don't want it to write to our
             # local repository directory so we expose it as a read-only directory to pacman.
-            "--ro-bind", context.packages, "/var/cache/pacman/mkosi",
+            Mount(context.packages, "/var/cache/pacman/mkosi", ro=True),
         ]
 
         if (context.root / "var/lib/pacman/local").exists():
             # pacman reuses the same directory for the sync databases and the local database containing the list of
             # installed packages. The former should go in the cache directory, the latter should go in the image, so we
             # bind mount the local directory from the image to make sure that happens.
-            mounts += ["--bind", context.root / "var/lib/pacman/local", "/var/lib/pacman/local"]
+            mounts += [Mount(context.root / "var/lib/pacman/local", "/var/lib/pacman/local")]
 
         if (
             (context.config.tools() / "etc/makepkg.conf").exists() and
             not (context.pkgmngr / "etc/makepkg.conf").exists()
         ):
-            mounts += ["--ro-bind", context.config.tools() / "etc/makepkg.conf", "/etc/makepkg.conf"]
+            mounts += [Mount(context.config.tools() / "etc/makepkg.conf", "/etc/makepkg.conf", ro=True)]
 
         return mounts
 
@@ -160,12 +160,8 @@ class Pacman(PackageManager):
                 sandbox=(
                     context.sandbox(
                         network=True,
-                        options=[
-                            "--bind", context.root, context.root,
-                            *cls.mounts(context),
-                            *sources,
-                            "--chdir", "/work/src",
-                        ],
+                        mounts=[Mount(context.root, context.root), *cls.mounts(context), *sources],
+                        options=["--dir", "/work/src", "--chdir", "/work/src"],
                     ) + (apivfs_cmd(context.root) if apivfs else [])
                 ),
                 env=context.config.environment,
@@ -185,7 +181,7 @@ class Pacman(PackageManager):
                 context.packages / "mkosi.db.tar",
                 *sorted(context.packages.glob("*.pkg.tar*"), key=lambda p: GenericVersion(Path(p).name))
             ],
-            sandbox=context.sandbox(options=["--bind", context.packages, context.packages]),
+            sandbox=context.sandbox(mounts=[Mount(context.packages, context.packages)]),
         )
 
         (context.pkgmngr / "etc/mkosi-local.conf").write_text(
index 6fb7ed192dddfd1259dd0aa6e1b893a99953b236..422ff7743a24fb80908646398fd8613e735a7da9 100644 (file)
@@ -11,7 +11,7 @@ from mkosi.installer import PackageManager
 from mkosi.installer.rpm import RpmRepository, rpm_cmd
 from mkosi.mounts import finalize_source_mounts
 from mkosi.run import run
-from mkosi.sandbox import apivfs_cmd
+from mkosi.sandbox import Mount, apivfs_cmd
 from mkosi.types import _FILE, CompletedProcess, PathString
 
 
@@ -131,12 +131,8 @@ class Zypper(PackageManager):
                 sandbox=(
                     context.sandbox(
                         network=True,
-                        options=[
-                            "--bind", context.root, context.root,
-                            *cls.mounts(context),
-                            *sources,
-                            "--chdir", "/work/src",
-                        ],
+                        mounts=[Mount(context.root, context.root), *cls.mounts(context), *sources],
+                        options=["--dir", "/work/src", "--chdir", "/work/src"],
                     ) + (apivfs_cmd(context.root) if apivfs else [])
                 ),
                 env=context.config.environment,
@@ -150,7 +146,7 @@ class Zypper(PackageManager):
     @classmethod
     def createrepo(cls, context: Context) -> None:
         run(["createrepo_c", context.packages],
-            sandbox=context.sandbox(options=["--bind", context.packages, context.packages]))
+            sandbox=context.sandbox(mounts=[Mount(context.packages, context.packages)]))
 
         (context.pkgmngr / "etc/zypp/repos.d/mkosi-local.repo").write_text(
             textwrap.dedent(
index ac31bc159d816e7e35122a9d714ea23f7393ac35..1c958d3d593dde41c91b726c88bb2fabac38ecbc 100644 (file)
@@ -9,7 +9,7 @@ from pathlib import Path
 
 from mkosi.log import complete_step, log_step
 from mkosi.run import run
-from mkosi.sandbox import SandboxProtocol, nosandbox
+from mkosi.sandbox import Mount, SandboxProtocol, nosandbox
 
 
 def loaded_modules() -> list[str]:
@@ -91,7 +91,7 @@ def resolve_module_dependencies(
         info += run(
             ["modinfo", "--basedir", root, "--set-version", kver, "--null", *chunk],
             stdout=subprocess.PIPE,
-            sandbox=sandbox(options=["--ro-bind", root, root])
+            sandbox=sandbox(mounts=[Mount(root, root, ro=True)]),
         ).stdout.strip()
 
     log_step("Calculating required kernel modules and firmware")
index 325cba30a86a0329a56bebb3a5d90d505f285435..4502dca06e90e6438c35b7e715e1504dab8203c3 100644 (file)
@@ -14,6 +14,7 @@ from mkosi.distributions import PackageType
 from mkosi.installer.apt import Apt
 from mkosi.log import complete_step
 from mkosi.run import run
+from mkosi.sandbox import Mount
 
 
 @dataclasses.dataclass
@@ -110,7 +111,7 @@ class Manifest:
                 "--queryformat", r"%{NEVRA}\t%{SOURCERPM}\t%{NAME}\t%{ARCH}\t%{LONGSIZE}\t%{INSTALLTIME}\n",
             ],
             stdout=subprocess.PIPE,
-            sandbox=self.context.sandbox(options=["--ro-bind", self.context.root, self.context.root]),
+            sandbox=self.context.sandbox(mounts=[Mount(self.context.root, self.context.root)]),
         )
 
         packages = sorted(c.stdout.splitlines())
@@ -156,7 +157,7 @@ class Manifest:
                     ],
                     stdout=subprocess.PIPE,
                     stderr=subprocess.DEVNULL,
-                    sandbox=self.context.sandbox(options=["--ro-bind", self.context.root, self.context.root]),
+                    sandbox=self.context.sandbox(mounts=[Mount(self.context.root, self.context.root, ro=True)]),
                 )
                 changelog = c.stdout.strip()
                 source = SourcePackageManifest(srpm, changelog)
@@ -174,7 +175,7 @@ class Manifest:
                     r'${Package}\t${source:Package}\t${Version}\t${Architecture}\t${Installed-Size}\t${db-fsys:Last-Modified}\n',
             ],
             stdout=subprocess.PIPE,
-            sandbox=self.context.sandbox(options=["--ro-bind", self.context.root, self.context.root]),
+            sandbox=self.context.sandbox(mounts=[Mount(self.context.root, self.context.root, ro=True)]),
         )
 
         packages = sorted(c.stdout.splitlines())
index 246fbb72c8f9a75728a43f5c9e9643de7588d013..788120b94b0aeaab3eec71a3565c391e480cd324 100644 (file)
@@ -11,6 +11,7 @@ from typing import Optional
 
 from mkosi.config import Config
 from mkosi.run import run
+from mkosi.sandbox import Mount
 from mkosi.types import PathString
 from mkosi.util import umask
 from mkosi.versioncomp import GenericVersion
@@ -123,16 +124,12 @@ def mount_overlay(
 
 
 @contextlib.contextmanager
-def finalize_source_mounts(config: Config, *, ephemeral: bool) -> Iterator[list[PathString]]:
+def finalize_source_mounts(config: Config, *, ephemeral: bool) -> Iterator[list[Mount]]:
     with contextlib.ExitStack() as stack:
-        mounts = (
+        sources = (
             (stack.enter_context(mount_overlay([source])) if ephemeral else source, target)
             for source, target
             in {t.with_prefix(Path("/work/src")) for t in config.build_sources}
         )
 
-        options: list[PathString] = ["--dir", "/work/src"]
-        for src, target in sorted(mounts, key=lambda s: s[1]):
-            options += ["--bind", src, target]
-
-        yield options
+        yield [Mount(src, target) for src, target in sorted(sources, key=lambda s: s[1])]
index f2fd3cc4a4a4e5c1fe3361661330dedb0af9011f..7168b9a8c053c951afa3bd584cfdcc31c6c10f1b 100644 (file)
@@ -7,7 +7,7 @@ from typing import Any, Optional
 
 from mkosi.log import die
 from mkosi.run import run
-from mkosi.sandbox import SandboxProtocol, nosandbox
+from mkosi.sandbox import Mount, SandboxProtocol, nosandbox
 
 
 @dataclasses.dataclass(frozen=True)
@@ -37,7 +37,7 @@ def find_partitions(image: Path, *, sandbox: SandboxProtocol = nosandbox) -> lis
             ["systemd-repart", "--json=short", image],
             stdout=subprocess.PIPE,
             stderr=subprocess.DEVNULL,
-            sandbox=sandbox(options=["--ro-bind", image, image]),
+            sandbox=sandbox(mounts=[Mount(image, image, ro=True)]),
         ).stdout
     )
     return [Partition.from_dict(d) for d in output]
index 4239dde74193bc847266f34b16fc7392f8bee429..95079bb51c6fee33677780edb776f54123522fe5 100644 (file)
@@ -37,6 +37,7 @@ from mkosi.config import (
 from mkosi.log import ARG_DEBUG, die
 from mkosi.partition import finalize_root, find_partitions
 from mkosi.run import AsyncioThread, find_binary, fork_and_wait, run, spawn
+from mkosi.sandbox import Mount
 from mkosi.tree import copy_tree, rmtree
 from mkosi.types import PathString
 from mkosi.user import INVOKING_USER, become_root
@@ -152,7 +153,7 @@ class KernelType(StrEnum):
         type = run(
             ["bootctl", "kernel-identify", path],
             stdout=subprocess.PIPE,
-            sandbox=config.sandbox(options=["--ro-bind", path, path]),
+            sandbox=config.sandbox(mounts=[Mount(path, path, ro=True)]),
         ).stdout.strip()
 
         try:
@@ -253,7 +254,7 @@ def start_swtpm(config: Config) -> Iterator[Path]:
     with tempfile.TemporaryDirectory(prefix="mkosi-swtpm") as state:
         # swtpm_setup is noisy and doesn't have a --quiet option so we pipe it's stdout to /dev/null.
         run(["swtpm_setup", "--tpm-state", state, "--tpm2", "--pcr-banks", "sha256", "--config", "/dev/null"],
-            sandbox=config.sandbox(options=["--bind", state, state]),
+            sandbox=config.sandbox(mounts=[Mount(state, state)]),
             stdout=None if ARG_DEBUG.get() else subprocess.DEVNULL)
 
         cmdline = ["swtpm", "socket", "--tpm2", "--tpmstate", f"dir={state}"]
@@ -270,7 +271,7 @@ def start_swtpm(config: Config) -> Iterator[Path]:
             with spawn(
                 cmdline,
                 pass_fds=(sock.fileno(),),
-                sandbox=config.sandbox(options=["--bind", state, state]),
+                sandbox=config.sandbox(mounts=[Mount(state, state)]),
             ) as proc:
                 try:
                     yield path
@@ -341,12 +342,8 @@ def start_virtiofsd(config: Config, directory: Path, *, uidmap: bool) -> Iterato
             group=INVOKING_USER.gid if uidmap else None,
             preexec_fn=become_root if not uidmap else None,
             sandbox=config.sandbox(
-                options=[
-                    "--uid", "0",
-                    "--gid", "0",
-                    "--cap-add", "all",
-                    "--bind", directory, directory,
-                ],
+                mounts=[Mount(directory, directory)],
+                options=["--uid", "0", "--gid", "0", "--cap-add", "all"],
             ),
         ) as proc:
             try:
@@ -491,9 +488,9 @@ def finalize_firmware_variables(config: Config, ovmf: OvmfConfig, stack: context
                 "--loglevel", "WARNING",
             ],
             sandbox=config.sandbox(
-                options=[
-                    "--bind", ovmf_vars.name, ovmf_vars.name,
-                    "--ro-bind", config.secure_boot_certificate, config.secure_boot_certificate,
+                mounts=[
+                    Mount(ovmf_vars.name, ovmf_vars.name),
+                    Mount(config.secure_boot_certificate, config.secure_boot_certificate, ro=True),
                 ],
             ),
         )
@@ -698,7 +695,7 @@ def run_qemu(args: Args, config: Config) -> None:
                     "--copy-from", src,
                     fname,
                 ],
-                sandbox=config.sandbox(options=["--bind", fname.parent, fname.parent, "--ro-bind", src, src]),
+                sandbox=config.sandbox(mounts=[Mount(fname.parent, fname.parent), Mount(src, src, ro=True)]),
             )
             stack.callback(lambda: fname.unlink())
         elif config.ephemeral and config.output_format not in (OutputFormat.cpio, OutputFormat.uki):
@@ -719,7 +716,7 @@ def run_qemu(args: Args, config: Config) -> None:
                     "--offline=yes",
                     fname,
                 ],
-                sandbox=config.sandbox(options=["--bind", fname, fname]),
+                sandbox=config.sandbox(mounts=[Mount(fname, fname)]),
             )
 
         if (
@@ -787,7 +784,7 @@ def run_qemu(args: Args, config: Config) -> None:
             run(
                 [f"mkfs.{fs}", "-L", "scratch", *extra.split(), scratch.name],
                 stdout=subprocess.DEVNULL,
-                sandbox=config.sandbox(options=["--bind", scratch.name, scratch.name]),
+                sandbox=config.sandbox(mounts=[Mount(scratch.name, scratch.name)]),
             )
             cmdline += [
                 "-drive", f"if=none,id=scratch,file={scratch.name},format=raw",
index 79c8e9485766c0f74209d65605898e305d44823a..350240ca5054a76b179f0e63babbdb950fc4951e 100644 (file)
@@ -5,18 +5,45 @@ import os
 import uuid
 from collections.abc import Sequence
 from pathlib import Path
-from typing import Optional, Protocol
+from typing import NamedTuple, Optional, Protocol
 
 from mkosi.types import PathString
 from mkosi.user import INVOKING_USER
 from mkosi.util import flatten, one_zero, startswith
 
 
+class Mount(NamedTuple):
+    src: PathString
+    dst: PathString
+    devices: bool = False
+    ro: bool = False
+    required: bool = True
+
+    def __hash__(self) -> int:
+        return hash((Path(self.src), Path(self.dst), self.devices, self.ro, self.required))
+
+    def __eq__(self, other: object) -> bool:
+        if not isinstance(other, Mount):
+            return False
+
+        return self.__hash__() == other.__hash__()
+
+    def options(self) -> list[str]:
+        if self.devices:
+            opt = "--dev-bind" if self.required else "--dev-bind-try"
+        elif self.ro:
+            opt = "--ro-bind" if self.required else "--ro-bind-try"
+        else:
+            opt = "--bind" if self.required else "--bind-try"
+
+        return [opt, os.fspath(self.src), os.fspath(self.dst)]
+
+
 class SandboxProtocol(Protocol):
-    def __call__(self, *, options: Sequence[PathString] = ()) -> list[PathString]: ...
+    def __call__(self, *, mounts: Sequence[Mount] = ()) -> list[PathString]: ...
 
 
-def nosandbox(*, options: Sequence[PathString] = ()) -> list[PathString]:
+def nosandbox(*, mounts: Sequence[Mount] = ()) -> list[PathString]:
     return []
 
 
@@ -37,21 +64,19 @@ def have_effective_cap(capability: Capability) -> bool:
     return (int(hexcap, 16) & (1 << capability.value)) != 0
 
 
-def finalize_passwd_mounts(root: Path) -> list[PathString]:
+def finalize_passwd_mounts(root: Path) -> list[Mount]:
     """
     If passwd or a related file exists in the apivfs directory, bind mount it over the host files while we
     run the command, to make sure that the command we run uses user/group information from the apivfs
     directory instead of from the host.
     """
-    options: list[PathString] = []
-
-    for f in ("passwd", "group", "shadow", "gshadow"):
-        options += ["--ro-bind-try", root / "etc" / f, f"/etc/{f}"]
-
-    return options
+    return [
+        Mount(root / "etc" / f, f"/etc/{f}", ro=True, required=False)
+        for f in ("passwd", "group", "shadow", "gshadow")
+    ]
 
 
-def finalize_crypto_mounts(tools: Path = Path("/")) -> list[PathString]:
+def finalize_crypto_mounts(tools: Path = Path("/")) -> list[Mount]:
     mounts = [
         (tools / subdir, Path("/") / subdir)
         for subdir in (
@@ -64,11 +89,11 @@ def finalize_crypto_mounts(tools: Path = Path("/")) -> list[PathString]:
         if (tools / subdir).exists()
     ]
 
-    return flatten(
-        ["--ro-bind", src, target]
+    return [
+        Mount(src, target, ro=True)
         for src, target
         in sorted(set(mounts), key=lambda s: s[1])
-    )
+    ]
 
 
 def sandbox_cmd(
@@ -78,6 +103,7 @@ def sandbox_cmd(
     scripts: Optional[Path] = None,
     tools: Path = Path("/"),
     relaxed: bool = False,
+    mounts: Sequence[Mount] = (),
     options: Sequence[PathString] = (),
 ) -> list[PathString]:
     cmdline: list[PathString] = []
@@ -93,7 +119,6 @@ def sandbox_cmd(
 
     cmdline += [
         "bwrap",
-        "--ro-bind", tools / "usr", "/usr",
         *(["--unshare-net"] if not network and have_effective_cap(Capability.CAP_NET_ADMIN) else []),
         "--die-with-parent",
         "--proc", "/proc",
@@ -101,9 +126,10 @@ def sandbox_cmd(
         # We mounted a subdirectory of TMPDIR to /var/tmp so we unset TMPDIR so that /tmp or /var/tmp are used instead.
         "--unsetenv", "TMPDIR",
     ]
+    allmounts = [Mount(tools / "usr", "/usr", ro=True)]
 
     if relaxed:
-        cmdline += ["--bind", "/tmp", "/tmp"]
+        allmounts += [Mount("/tmp", "/tmp")]
     else:
         cmdline += [
             "--tmpfs", "/tmp",
@@ -111,13 +137,13 @@ def sandbox_cmd(
         ]
 
     if (tools / "nix/store").exists():
-        cmdline += ["--bind", tools / "nix/store", "/nix/store"]
+        allmounts += [Mount(tools / "nix/store", "/nix/store")]
 
     if devices or relaxed:
-        cmdline += [
-            "--bind", "/sys", "/sys",
-            "--bind", "/run", "/run",
-            "--dev-bind", "/dev", "/dev",
+        allmounts += [
+            Mount("/sys", "/sys"),
+            Mount("/run", "/run"),
+            Mount("/dev", "/dev", devices=True),
         ]
     else:
         cmdline += ["--dev", "/dev"]
@@ -127,7 +153,7 @@ def sandbox_cmd(
 
         for d in dirs:
             if Path(d).exists():
-                cmdline += ["--bind", d, d]
+                allmounts += [Mount(d, d)]
 
         if len(Path.cwd().parents) >= 2:
             # `Path.parents` only supports slices and negative indexing from Python 3.10 onwards.
@@ -139,23 +165,21 @@ def sandbox_cmd(
             d = ""
 
         if d and d not in (*dirs, "/home", "/usr", "/nix", "/tmp"):
-            cmdline += ["--bind", d, d]
+            allmounts += [Mount(d, d)]
 
     if vartmp:
-        cmdline += ["--bind", vartmp, "/var/tmp"]
+        allmounts += [Mount(vartmp, "/var/tmp")]
 
     for d in ("bin", "sbin", "lib", "lib32", "lib64"):
         if (p := tools / d).is_symlink():
             cmdline += ["--symlink", p.readlink(), Path("/") / p.relative_to(tools)]
         elif p.is_dir():
-            cmdline += ["--ro-bind", p, Path("/") / p.relative_to(tools)]
+            allmounts += [Mount(p, Path("/") / p.relative_to(tools), ro=True)]
 
     path = "/usr/bin:/usr/sbin" if tools != Path("/") else os.environ["PATH"]
 
-    cmdline += [
-        "--setenv", "PATH", f"/scripts:{path}",
-        *options,
-    ]
+    cmdline += ["--setenv", "PATH", f"/scripts:{path}", *options]
+    allmounts = [*allmounts, *mounts]
 
     if not relaxed:
         cmdline += ["--symlink", "../proc/self/mounts", "/etc/mtab"]
@@ -166,13 +190,15 @@ def sandbox_cmd(
     # already exists on the host as otherwise we'd modify the host's /etc by creating the mountpoint ourselves (or
     # fail when trying to create it).
     if (tools / "etc/alternatives").exists() and (not relaxed or Path("/etc/alternatives").exists()):
-        cmdline += ["--ro-bind", tools / "etc/alternatives", "/etc/alternatives"]
+        allmounts += [Mount(tools / "etc/alternatives", "/etc/alternatives", ro=True)]
 
     if scripts:
-        cmdline += ["--ro-bind", scripts, "/scripts"]
+        allmounts += [Mount(scripts, "/scripts", ro=True)]
 
     if network and not relaxed and Path("/etc/resolv.conf").exists():
-        cmdline += ["--bind", "/etc/resolv.conf", "/etc/resolv.conf"]
+        allmounts += [Mount("/etc/resolv.conf", "/etc/resolv.conf")]
+
+    cmdline += flatten(mount.options() for mount in allmounts)
 
     # bubblewrap creates everything with a restricted mode so relax stuff as needed.
     ops = []
@@ -204,7 +230,7 @@ def apivfs_cmd(root: Path) -> list[PathString]:
         "--ro-bind-try", root / "etc/machine-id", root / "etc/machine-id",
         # Nudge gpg to create its sockets in /run by making sure /run/user/0 exists.
         "--dir", root / "run/user/0",
-        *finalize_passwd_mounts(root),
+        *flatten(mount.options() for mount in finalize_passwd_mounts(root)),
         "sh", "-c",
         f"chmod 1777 {root / 'tmp'} {root / 'var/tmp'} {root / 'dev/shm'} && "
         f"chmod 755 {root / 'run'} && "
index 079b7adef929189517ca42e7437dff2b61937832..ab4b80a19e6f67f8dfdbeb2af0f8b33cebbe8c9b 100644 (file)
@@ -11,15 +11,14 @@ from pathlib import Path
 from mkosi.config import ConfigFeature
 from mkosi.log import ARG_DEBUG, die
 from mkosi.run import find_binary, run
-from mkosi.sandbox import SandboxProtocol, nosandbox
+from mkosi.sandbox import Mount, SandboxProtocol, nosandbox
 from mkosi.types import PathString
-from mkosi.util import flatten
 from mkosi.versioncomp import GenericVersion
 
 
 def statfs(path: Path, *, sandbox: SandboxProtocol = nosandbox) -> str:
     return run(["stat", "--file-system", "--format", "%T", path],
-               sandbox=sandbox(options=["--ro-bind", path, path]), stdout=subprocess.PIPE).stdout.strip()
+               sandbox=sandbox(mounts=[Mount(path, path, ro=True)]), stdout=subprocess.PIPE).stdout.strip()
 
 
 def is_subvolume(path: Path, *, sandbox: SandboxProtocol = nosandbox) -> bool:
@@ -51,7 +50,7 @@ def make_tree(
 
     if use_subvolumes != ConfigFeature.disabled and find_binary("btrfs", root=tools) is not None:
         result = run(["btrfs", "subvolume", "create", path],
-                     sandbox=sandbox(options=["--bind", path.parent, path.parent]),
+                     sandbox=sandbox(mounts=[Mount(path.parent, path.parent)]),
                      check=use_subvolumes == ConfigFeature.enabled).returncode
     else:
         result = 1
@@ -105,7 +104,7 @@ def copy_tree(
     if cp_version(sandbox=sandbox) >= "9.5":
         copy += ["--keep-directory-symlink"]
 
-    options: list[PathString] = ["--ro-bind", src, src, "--bind", dst.parent, dst.parent]
+    mounts = [Mount(src, src, ro=True), Mount(dst.parent, dst.parent)]
 
     # If the source and destination are both directories, we want to merge the source directory with the
     # destination directory. If the source if a file and the destination is a directory, we want to copy
@@ -126,7 +125,7 @@ def copy_tree(
             if not preserve
             else contextlib.nullcontext()
         ):
-            run(copy, sandbox=sandbox(options=options))
+            run(copy, sandbox=sandbox(mounts=mounts))
         return dst
 
     # btrfs can't snapshot to an existing directory so make sure the destination does not exist.
@@ -134,14 +133,14 @@ def copy_tree(
         dst.rmdir()
 
     result = run(["btrfs", "subvolume", "snapshot", src, dst],
-                 check=use_subvolumes == ConfigFeature.enabled, sandbox=sandbox(options=options)).returncode
+                 check=use_subvolumes == ConfigFeature.enabled, sandbox=sandbox(mounts=mounts)).returncode
     if result != 0:
         with (
             preserve_target_directories_stat(src, dst)
             if not preserve
             else contextlib.nullcontext()
         ):
-            run(copy, sandbox=sandbox(options=options))
+            run(copy, sandbox=sandbox(mounts=mounts))
 
     return dst
 
@@ -158,7 +157,7 @@ def rmtree(*paths: Path, tools: Path = Path("/"), sandbox: SandboxProtocol = nos
         # btrfs filesystem is mounted with user_subvol_rm_allowed.
         run(["btrfs", "subvolume", "delete", *subvolumes],
             check=False,
-            sandbox=sandbox(options=flatten(["--bind", p, p] for p in parents)),
+            sandbox=sandbox(mounts=[Mount(p, p) for p in parents]),
             stdout=subprocess.DEVNULL if not ARG_DEBUG.get() else None,
             stderr=subprocess.DEVNULL if not ARG_DEBUG.get() else None)
 
@@ -167,7 +166,7 @@ def rmtree(*paths: Path, tools: Path = Path("/"), sandbox: SandboxProtocol = nos
         parents = sorted(set(p.parent for p in paths))
         parents = [p for p in parents if all(p == q or not p.is_relative_to(q) for q in parents)]
         run(["rm", "-rf", "--", *paths],
-            sandbox=sandbox(options=flatten(["--bind", p, p] for p in parents)))
+            sandbox=sandbox(mounts=[Mount(p, p) for p in parents]))
 
 
 def move_tree(
index 8ed0fe879bbade88de2284bf5b2ec67c296346de..1c6f9a5ff05662da7bb291c48037522e789e6194 100644 (file)
@@ -20,6 +20,7 @@ from mkosi.qemu import (
     find_ovmf_firmware,
 )
 from mkosi.run import run
+from mkosi.sandbox import Mount
 from mkosi.types import PathString
 from mkosi.util import flock_or_die
 
@@ -93,7 +94,7 @@ def run_vmspawn(args: Args, config: Config) -> None:
                     "--offline=yes",
                     fname,
                 ],
-                sandbox=config.sandbox(options=["--bind", fname, fname]),
+                sandbox=config.sandbox(mounts=[Mount(fname, fname)]),
             )
 
         kcl = config.kernel_command_line_extra