From: Daan De Meyer Date: Fri, 15 Mar 2024 08:59:04 +0000 (+0100) Subject: Introduce Mount named tuple to pass mounts to sandbox_cmd() X-Git-Tag: v23~90^2~1 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=ab0d93efc8286d3fcb665d98ac867feaa3b9e591;p=thirdparty%2Fmkosi.git Introduce Mount named tuple to pass mounts to sandbox_cmd() No change in behavior, but this will allow us to post-process mounts in sandbox_cmd() in later commits. --- diff --git a/mkosi/__init__.py b/mkosi/__init__.py index 06fa4f009..6e6b68a54 100644 --- a/mkosi/__init__.py +++ b/mkosi/__init__.py @@ -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: diff --git a/mkosi/archive.py b/mkosi/archive.py index 6ffa3930d..32a614c04 100644 --- a/mkosi/archive.py +++ b/mkosi/archive.py @@ -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)]), ) diff --git a/mkosi/config.py b/mkosi/config.py index b7ff56309..26030e68a 100644 --- a/mkosi/config.py +++ b/mkosi/config.py @@ -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: diff --git a/mkosi/context.py b/mkosi/context.py index c0b85e485..6b630fbfa 100644 --- a/mkosi/context.py +++ b/mkosi/context.py @@ -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 []), ], ) + ( [ diff --git a/mkosi/distributions/debian.py b/mkosi/distributions/debian.py index 568e8d258..4c16ca450 100644 --- a/mkosi/distributions/debian.py +++ b/mkosi/distributions/debian.py @@ -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() diff --git a/mkosi/distributions/opensuse.py b/mkosi/distributions/opensuse.py index b37bc72b2..8451e2230 100644 --- a/mkosi/distributions/opensuse.py +++ b/mkosi/distributions/opensuse.py @@ -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() diff --git a/mkosi/installer/__init__.py b/mkosi/installer/__init__.py index eb2f12bfd..04340b427 100644 --- a/mkosi/installer/__init__.py +++ b/mkosi/installer/__init__.py @@ -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 diff --git a/mkosi/installer/apt.py b/mkosi/installer/apt.py index 440d34834..7cfa84cf2 100644 --- a/mkosi/installer/apt.py +++ b/mkosi/installer/apt.py @@ -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], ), ) diff --git a/mkosi/installer/dnf.py b/mkosi/installer/dnf.py index e74fd48c2..7b6fc0ede 100644 --- a/mkosi/installer/dnf.py +++ b/mkosi/installer/dnf.py @@ -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( diff --git a/mkosi/installer/pacman.py b/mkosi/installer/pacman.py index b1b6282c3..e5faeff22 100644 --- a/mkosi/installer/pacman.py +++ b/mkosi/installer/pacman.py @@ -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( diff --git a/mkosi/installer/zypper.py b/mkosi/installer/zypper.py index 6fb7ed192..422ff7743 100644 --- a/mkosi/installer/zypper.py +++ b/mkosi/installer/zypper.py @@ -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( diff --git a/mkosi/kmod.py b/mkosi/kmod.py index ac31bc159..1c958d3d5 100644 --- a/mkosi/kmod.py +++ b/mkosi/kmod.py @@ -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") diff --git a/mkosi/manifest.py b/mkosi/manifest.py index 325cba30a..4502dca06 100644 --- a/mkosi/manifest.py +++ b/mkosi/manifest.py @@ -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()) diff --git a/mkosi/mounts.py b/mkosi/mounts.py index 246fbb72c..788120b94 100644 --- a/mkosi/mounts.py +++ b/mkosi/mounts.py @@ -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])] diff --git a/mkosi/partition.py b/mkosi/partition.py index f2fd3cc4a..7168b9a8c 100644 --- a/mkosi/partition.py +++ b/mkosi/partition.py @@ -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] diff --git a/mkosi/qemu.py b/mkosi/qemu.py index 4239dde74..95079bb51 100644 --- a/mkosi/qemu.py +++ b/mkosi/qemu.py @@ -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", diff --git a/mkosi/sandbox.py b/mkosi/sandbox.py index 79c8e9485..350240ca5 100644 --- a/mkosi/sandbox.py +++ b/mkosi/sandbox.py @@ -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'} && " diff --git a/mkosi/tree.py b/mkosi/tree.py index 079b7adef..ab4b80a19 100644 --- a/mkosi/tree.py +++ b/mkosi/tree.py @@ -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( diff --git a/mkosi/vmspawn.py b/mkosi/vmspawn.py index 8ed0fe879..1c6f9a5ff 100644 --- a/mkosi/vmspawn.py +++ b/mkosi/vmspawn.py @@ -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