]> git.ipfire.org Git - thirdparty/mkosi.git/commitdiff
Bind mount /etc from tools tree into relaxed sandbox 3749/head
authorDaan De Meyer <daan.j.demeyer@gmail.com>
Thu, 29 May 2025 14:20:54 +0000 (16:20 +0200)
committerDaan De Meyer <daan.j.demeyer@gmail.com>
Sun, 15 Jun 2025 20:12:44 +0000 (21:12 +0100)
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.

action.yaml
mkosi/__init__.py
mkosi/mounts.py
mkosi/resources/mkosi-tools/mkosi.skeleton/etc/resolv.conf [new file with mode: 0644]
mkosi/run.py
tests/test_initrd.py

index ced80781b21f6edf94ff61cde908f8ed410e9902..4d70f063699248a0a4842fb40a4c26aa1a611814 100644 (file)
@@ -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.
index 24c2c5ef90098b82244b0f9ef0d0e492378db064..23604f4c5274d1dd4457ce3baaf40564022f5fc2 100644 (file)
@@ -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,
index 863316965d7880ce25c5d462b8b81aa975c96763..f15791890347a1b234d334dce4f199d51467666e 100644 (file)
@@ -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 (file)
index 0000000..e69de29
index 05304d0fa585c9d93f7bad038cc4cbc728ca6cba..e6eac7b65e17835f9cb11c3babf6796d941aa6f7 100644 (file)
@@ -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,
index 6d80abb7efbdb89ae554ba517b405d32a1c8a8a1..143d49cfcb5d6ed031da450b2550f9d82a5844ed 100644 (file)
@@ -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"])