From: Daan De Meyer Date: Thu, 29 May 2025 14:20:54 +0000 (+0200) Subject: Bind mount /etc from tools tree into relaxed sandbox X-Git-Url: http://git.ipfire.org/gitweb/gitweb.cgi?a=commitdiff_plain;h=refs%2Fpull%2F3749%2Fhead;p=thirdparty%2Fmkosi.git Bind mount /etc from tools tree into relaxed sandbox Config from /etc often references stuff in /usr. Two examples I've encountered are shell config from /etc/profile and dnf5 blowing up when there's plugin configuration in /etc without the corresponding plugin being installed. To work around such issues, let's use /etc from the tools tree in the relaxed sandbox instead of /etc from the host. This also saves the user from having to create directories in their host's /etc to be able to use mkosi sandbox. --- diff --git a/action.yaml b/action.yaml index ced80781b..4d70f0636 100644 --- a/action.yaml +++ b/action.yaml @@ -62,11 +62,7 @@ runs: - name: Create missing mountpoints shell: bash run: | - for p in /etc/pki/ca-trust /etc/pki/tls /etc/ssl /etc/ca-certificates /var/lib/ca-certificates /etc/crypto-policies; do - if [[ ! -e "$p" ]]; then - sudo mkdir -p "$p" - fi - done + sudo mkdir -p /var/lib/ca-certificates # Both the unix-chkpwd and swtpm profiles are broken (https://gitlab.com/apparmor/apparmor/-/issues/402) so let's # just disable and remove apparmor completely. It's not relevant in this context anyway. diff --git a/mkosi/__init__.py b/mkosi/__init__.py index 24c2c5ef9..23604f4c5 100644 --- a/mkosi/__init__.py +++ b/mkosi/__init__.py @@ -4127,14 +4127,18 @@ def run_sandbox(args: Args, config: Config) -> None: die("Please specify a command to execute in the sandbox") mounts = finalize_certificate_mounts(config, relaxed=True) + if config.tools() != Path("/"): + for f in ("passwd", "group", "shadow", "gshadow"): + if Path(f"/etc/{f}").exists() and (config.tools() / "etc" / f).exists(): + mounts += ["--ro-bind", f"/etc/{f}", f"/etc/{f}"] - if config.tools() != Path("/") and (config.tools() / "etc/crypto-policies").exists(): - mounts += ["--ro-bind", config.tools() / "etc/crypto-policies", Path("/etc/crypto-policies")] + if Path("/etc/nsswitch.conf").exists() and (config.tools() / "etc/nsswitch.conf").exists(): + mounts += ["--ro-bind", "/etc/nsswitch.conf", "/etc/nsswitch.conf"] - # Since we reuse almost every top level directory from the host except /usr, the crypto mountpoints - # have to exist already in these directories or we'll fail with a permission error. Let's check this - # early and show a better error and a suggestion on how users can fix this issue. We use slice - # notation to get every 3rd item from the mounts list which is the destination path. + # Since we reuse almost every top level directory from the host except /usr and /etc, the crypto + # mountpoints have to exist already in these directories or we'll fail with a permission error. Let's + # check this early and show a better error and a suggestion on how users can fix this issue. We use + # slice notation to get every 3rd item from the mounts list which is the destination path. for dst in mounts[2::3]: if not Path(dst).exists(): die( @@ -4948,6 +4952,22 @@ def run_build( ) +def ensure_tools_tree_has_etc_resolv_conf(config: Config) -> None: + if not config.tools_tree: + return + + # We can't bind mount in the hosts's /etc/resolv.conf if this file doesn't exist without making the + # entirety of /etc writable or messing around with overlayfs, so let's just ensure it exists. + path = config.tools_tree / "etc/resolv.conf" + + if not path.is_symlink() and not path.exists(): + die( + f"Tools tree {config.tools_tree} is missing /etc/resolv.conf", + hint="If you're using a default tools tree, run mkosi -f clean to remove the old tools tree " + "without /etc/resolv.conf", + ) + + def run_verb(args: Args, tools: Optional[Config], images: Sequence[Config], *, resources: Path) -> None: images = list(images) @@ -5081,9 +5101,15 @@ def run_verb(args: Args, tools: Optional[Config], images: Sequence[Config], *, r metadata_dir=Path(metadata_dir), ) + resolv = tools.output_dir_or_cwd() / tools.output / "etc/resolv.conf" + if not resolv.is_symlink() and not resolv.exists(): + resolv.touch() + _, _, manifest = cache_tree_paths(tools) manifest.write_text(dump_json(tools.cache_manifest())) + ensure_tools_tree_has_etc_resolv_conf(last) + if args.verb.needs_tools(): return { Verb.ssh: run_ssh, diff --git a/mkosi/mounts.py b/mkosi/mounts.py index 863316965..f15791890 100644 --- a/mkosi/mounts.py +++ b/mkosi/mounts.py @@ -138,15 +138,18 @@ def finalize_certificate_mounts(config: Config, relaxed: bool = False) -> list[P root = config.tools() if config.tools_tree_certificates else Path("/") if not relaxed or root != Path("/"): - mounts += [ - (root / subdir, Path("/") / subdir) - for subdir in ( + subdirs = [Path("var/lib/ca-certificates")] + if not relaxed: + subdirs += [ Path("etc/pki/ca-trust"), Path("etc/pki/tls"), Path("etc/ssl"), Path("etc/ca-certificates"), - Path("var/lib/ca-certificates"), - ) + ] + + mounts += [ + (root / subdir, Path("/") / subdir) + for subdir in subdirs if (root / subdir).exists() and any(p for p in (root / subdir).rglob("*") if not p.is_dir()) ] diff --git a/mkosi/resources/mkosi-tools/mkosi.skeleton/etc/resolv.conf b/mkosi/resources/mkosi-tools/mkosi.skeleton/etc/resolv.conf new file mode 100644 index 000000000..e69de29bb diff --git a/mkosi/run.py b/mkosi/run.py index 05304d0fa..e6eac7b65 100644 --- a/mkosi/run.py +++ b/mkosi/run.py @@ -504,14 +504,6 @@ def sandbox_cmd( elif p.is_dir(): cmdline += ["--ro-bind", p, Path("/") / p.relative_to(tools)] - # If we're using /usr from a tools tree, we have to use /etc/alternatives and /etc/ld.so.cache from - # the tools tree as well if they exists since those are directly related to /usr. In relaxed mode, we - # only do this if the mountpoint 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). - for p in (Path("etc/alternatives"), Path("etc/ld.so.cache")): - if (tools / p).exists() and (not relaxed or (Path("/") / p).exists()): - cmdline += ["--ro-bind", tools / p, Path("/") / p] - if (tools / "nix/store").exists(): cmdline += ["--bind", tools / "nix/store", "/nix/store"] @@ -526,20 +518,14 @@ def sandbox_cmd( Path("/lib"), Path("/lib32"), Path("/lib64"), + Path("/etc"), ): if p.is_symlink(): cmdline += ["--symlink", p.readlink(), p] else: cmdline += ["--bind", p, p] - # /etc might be full of symlinks to /usr/share/factory, so make sure we use - # /usr/share/factory from the host and not from the tools tree. - if ( - tools != Path("/") - and (tools / "usr/share/factory").exists() - and (factory := Path("/usr/share/factory")).exists() - ): - cmdline += ["--bind", factory, factory] + cmdline += ["--ro-bind", tools / "etc", "/etc"] else: cmdline += [ "--dir", "/var/tmp", @@ -556,10 +542,17 @@ def sandbox_cmd( else: cmdline += ["--dev", "/dev"] - if network: - for p in (Path("/etc/resolv.conf"), Path("/run/systemd/resolve")): - if p.exists(): - cmdline += ["--ro-bind", p, p] + # If we're using /usr from a tools tree, we have to use /etc/alternatives and /etc/ld.so.cache + # from the tools tree as well if they exists since those are directly related to /usr. + for p in (Path("etc/alternatives"), Path("etc/ld.so.cache")): + if (tools / p).exists(): + cmdline += ["--ro-bind", tools / p, Path("/") / p] + + if network and (p := Path("/run/systemd/resolve")).exists(): + cmdline += ["--ro-bind", p, p] + + if network and (p := Path("/etc/resolv.conf")).exists(): + cmdline += ["--ro-bind", p, p] path = finalize_path( root=tools, diff --git a/tests/test_initrd.py b/tests/test_initrd.py index 6d80abb7e..143d49cfc 100644 --- a/tests/test_initrd.py +++ b/tests/test_initrd.py @@ -69,14 +69,14 @@ def test_initrd_lvm(config: ImageConfig) -> None: ).stdout.strip() stack.callback(lambda: run(["losetup", "--detach", lodev])) run(["sfdisk", "--label", "gpt", lodev], input="type=E6D6D379-F507-44C2-A23C-238F2A3DF928 bootable") - run(["lvm", "pvcreate", f"{lodev}p1"]) - run(["lvm", "pvs"]) - run(["lvm", "vgcreate", "-An", "vg_mkosi", f"{lodev}p1"]) - run(["lvm", "vgchange", "-ay", "vg_mkosi"]) - run(["lvm", "vgs"]) - stack.callback(lambda: run(["vgchange", "-an", "vg_mkosi"])) - run(["lvm", "lvcreate", "-An", "-l", "100%FREE", "-n", "lv0", "vg_mkosi"]) - run(["lvm", "lvs"]) + run(["lvm", "pvcreate", "--devicesfile", "", f"{lodev}p1"]) + run(["lvm", "pvs", "--devicesfile", ""]) + run(["lvm", "vgcreate", "--devicesfile", "", "-An", "vg_mkosi", f"{lodev}p1"]) + run(["lvm", "vgchange", "--devicesfile", "", "-ay", "vg_mkosi"]) + run(["lvm", "vgs", "--devicesfile", ""]) + stack.callback(lambda: run(["lvm", "vgchange", "--devicesfile", "", "-an", "vg_mkosi"])) + run(["lvm", "lvcreate", "--devicesfile", "", "-An", "-l", "100%FREE", "-n", "lv0", "vg_mkosi"]) + run(["lvm", "lvs", "--devicesfile", ""]) run(["udevadm", "wait", "--timeout=30", "/dev/vg_mkosi/lv0"]) run([f"mkfs.{image.config.distribution.filesystem()}", "-L", "root", "/dev/vg_mkosi/lv0"]) @@ -181,14 +181,14 @@ def test_initrd_luks_lvm(config: ImageConfig, passphrase: Path) -> None: run(["cryptsetup", "--key-file", passphrase, "luksOpen", f"{lodev}p1", "lvm_root"]) stack.callback(lambda: run(["cryptsetup", "close", "lvm_root"])) luks_uuid = run(["cryptsetup", "luksUUID", f"{lodev}p1"], stdout=subprocess.PIPE).stdout.strip() - run(["lvm", "pvcreate", "/dev/mapper/lvm_root"]) - run(["lvm", "pvs"]) - run(["lvm", "vgcreate", "-An", "vg_mkosi", "/dev/mapper/lvm_root"]) - run(["lvm", "vgchange", "-ay", "vg_mkosi"]) - run(["lvm", "vgs"]) - stack.callback(lambda: run(["vgchange", "-an", "vg_mkosi"])) - run(["lvm", "lvcreate", "-An", "-l", "100%FREE", "-n", "lv0", "vg_mkosi"]) - run(["lvm", "lvs"]) + run(["lvm", "pvcreate", "--devicesfile", "", "/dev/mapper/lvm_root"]) + run(["lvm", "pvs", "--devicesfile", ""]) + run(["lvm", "vgcreate", "--devicesfile", "", "-An", "vg_mkosi", "/dev/mapper/lvm_root"]) + run(["lvm", "vgchange", "--devicesfile", "", "-ay", "vg_mkosi"]) + run(["lvm", "vgs", "--devicesfile", ""]) + stack.callback(lambda: run(["lvm", "vgchange", "--devicesfile", "", "-an", "vg_mkosi"])) + run(["lvm", "lvcreate", "--devicesfile", "", "-An", "-l", "100%FREE", "-n", "lv0", "vg_mkosi"]) + run(["lvm", "lvs", "--devicesfile", ""]) run(["udevadm", "wait", "--timeout=30", "/dev/vg_mkosi/lv0"]) run([f"mkfs.{image.config.distribution.filesystem()}", "-L", "root", "/dev/vg_mkosi/lv0"])