We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
`mkosi qemu` crashes when `--directory` has only one path component
Starting with version 20 (`20.1-1~bpo12+1` from Debian bookworm-backports), `mkosi qemu` seems to assume that its config directory path always has at least 2 components. This leads to crashes like the following:

```
root@mkosi:~# mkdir /foo; mkosi --directory=/foo qemu
[...]
mke2fs 1.47.0 (5-Feb-2023)
Traceback (most recent call last):
  File "/usr/lib/python3/dist-packages/mkosi/run.py", line 162, in uncaught_exception_handler
    yield
  File "/usr/lib/python3.11/contextlib.py", line 81, in inner
    return func(*args, **kwds)
           ^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3/dist-packages/mkosi/__main__.py", line 34, in main
    run_verb(args, images)
  File "/usr/lib/python3/dist-packages/mkosi/__init__.py", line 3468, in run_verb
    {
  File "/usr/lib/python3/dist-packages/mkosi/qemu.py", line 830, in run_qemu
    sandbox=config.sandbox(network=True, devices=True, relaxed=True),
            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3/dist-packages/mkosi/config.py", line 1386, in sandbox
    return sandbox_cmd(
           ^^^^^^^^^^^^
  File "/usr/lib/python3/dist-packages/mkosi/sandbox.py", line 124, in sandbox_cmd
    if (d := os.fspath(list(Path.cwd().parents)[-2])) not in (*dirs, "/home", "/usr", "/nix", "/tmp"):
                       ~~~~~~~~~~~~~~~~~~~~~~~~^^^^
IndexError: list index out of range
```

I ran into this problem by using mkosi inside a Vagrant VM, which mounts the project directory to `/vagrant` by default. Changing that mountpoint to `/media/vagrant` works as a workaround.



--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `mkosi/sandbox.py`
Content:
```
1 # SPDX-License-Identifier: LGPL-2.1+
2 import enum
3 import logging
4 import os
5 import uuid
6 from collections.abc import Sequence
7 from pathlib import Path
8 from typing import Optional
9 
10 from mkosi.types import PathString
11 from mkosi.util import INVOKING_USER, flatten, one_zero
12 
13 
14 # https://github.com/torvalds/linux/blob/master/include/uapi/linux/capability.h
15 class Capability(enum.Enum):
16     CAP_NET_ADMIN = 12
17 
18 
19 def have_effective_cap(capability: Capability) -> bool:
20     for line in Path("/proc/self/status").read_text().splitlines():
21         if line.startswith("CapEff:"):
22             hexcap = line.removeprefix("CapEff:").strip()
23             break
24     else:
25         logging.warning(f"\"CapEff:\" not found in /proc/self/status, assuming we don't have {capability}")
26         return False
27 
28     return (int(hexcap, 16) & (1 << capability.value)) != 0
29 
30 
31 def finalize_passwd_mounts(root: Path) -> list[PathString]:
32     """
33     If passwd or a related file exists in the apivfs directory, bind mount it over the host files while we
34     run the command, to make sure that the command we run uses user/group information from the apivfs
35     directory instead of from the host.
36     """
37     options: list[PathString] = []
38 
39     for f in ("passwd", "group", "shadow", "gshadow"):
40         options += ["--ro-bind-try", root / "etc" / f, f"/etc/{f}"]
41 
42     return options
43 
44 
45 def finalize_crypto_mounts(tools: Path = Path("/")) -> list[PathString]:
46     mounts = [
47         (tools / subdir, Path("/") / subdir)
48         for subdir in (
49             Path("etc/pki"),
50             Path("etc/ssl"),
51             Path("etc/crypto-policies"),
52             Path("etc/ca-certificates"),
53             Path("etc/pacman.d/gnupg"),
54             Path("var/lib/ca-certificates"),
55         )
56         if (tools / subdir).exists()
57     ]
58 
59     return flatten(
60         ["--ro-bind", src, target]
61         for src, target
62         in sorted(set(mounts), key=lambda s: s[1])
63     )
64 
65 
66 def sandbox_cmd(
67     *,
68     network: bool = False,
69     devices: bool = False,
70     scripts: Optional[Path] = None,
71     tools: Path = Path("/"),
72     relaxed: bool = False,
73     options: Sequence[PathString] = (),
74 ) -> list[PathString]:
75     cmdline: list[PathString] = []
76 
77     if not relaxed:
78         # We want to use an empty subdirectory in the host's /var/tmp as the sandbox's /var/tmp. To make sure it only
79         # gets created when we run the sandboxed command and cleaned up when the sandboxed command exits, we create it
80         # using shell.
81         vartmp = f"/var/tmp/mkosi-var-tmp-{uuid.uuid4().hex[:16]}"
82         cmdline += ["sh", "-c", f"trap 'rm -rf {vartmp}' EXIT && mkdir --mode 1777 {vartmp} && $0 \"$@\""]
83     else:
84         vartmp = None
85 
86     cmdline += [
87         "bwrap",
88         "--ro-bind", tools / "usr", "/usr",
89         *(["--unshare-net"] if not network and have_effective_cap(Capability.CAP_NET_ADMIN) else []),
90         "--die-with-parent",
91         "--proc", "/proc",
92         "--setenv", "SYSTEMD_OFFLINE", one_zero(network),
93     ]
94 
95     if relaxed:
96         cmdline += ["--bind", "/tmp", "/tmp"]
97     else:
98         cmdline += [
99             "--tmpfs", "/tmp",
100             "--unshare-ipc",
101         ]
102 
103     if (tools / "nix/store").exists():
104         cmdline += ["--bind", tools / "nix/store", "/nix/store"]
105 
106     if devices or relaxed:
107         cmdline += [
108             "--bind", "/sys", "/sys",
109             "--bind", "/run", "/run",
110             "--dev-bind", "/dev", "/dev",
111         ]
112     else:
113         cmdline += ["--dev", "/dev"]
114 
115     if relaxed:
116         dirs = ("/etc", "/opt", "/srv", "/media", "/mnt", "/var", os.fspath(INVOKING_USER.home()))
117 
118         for d in dirs:
119             if Path(d).exists():
120                 cmdline += ["--bind", d, d]
121 
122         # `Path.parents` only supports slices and negative indexing from Python 3.10 onwards.
123         # TODO: Remove list() when we depend on Python 3.10 or newer.
124         if (d := os.fspath(list(Path.cwd().parents)[-2])) not in (*dirs, "/home", "/usr", "/nix", "/tmp"):
125             cmdline += ["--bind", d, d]
126 
127     if vartmp:
128         cmdline += ["--bind", vartmp, "/var/tmp"]
129 
130     for d in ("bin", "sbin", "lib", "lib32", "lib64"):
131         if (p := tools / d).is_symlink():
132             cmdline += ["--symlink", p.readlink(), Path("/") / p.relative_to(tools)]
133 
134     path = "/usr/bin:/usr/sbin" if tools != Path("/") else os.environ["PATH"]
135 
136     cmdline += [
137         "--setenv", "PATH", f"{scripts or ''}:{path}",
138         *options,
139     ]
140 
141     if not relaxed:
142         cmdline += ["--symlink", "../proc/self/mounts", "/etc/mtab"]
143 
144     # If we're using /usr from a tools tree, we have to use /etc/alternatives from the tools tree as well if it
145     # exists since that points directly back to /usr. Apply this after the options so the caller can mount
146     # something else to /etc without overriding this mount. In relaxed mode, we only do this if /etc/alternatives
147     # already exists on the host as otherwise we'd modify the host's /etc by creating the mountpoint ourselves (or
148     # fail when trying to create it).
149     if (tools / "etc/alternatives").exists() and (not relaxed or Path("/etc/alternatives").exists()):
150         cmdline += ["--ro-bind", tools / "etc/alternatives", "/etc/alternatives"]
151 
152     if scripts:
153         cmdline += ["--ro-bind", scripts, scripts]
154 
155     if network and not relaxed:
156         cmdline += ["--bind", "/etc/resolv.conf", "/etc/resolv.conf"]
157 
158     # bubblewrap creates everything with a restricted mode so relax stuff as needed.
159     ops = []
160     if not devices:
161         ops += ["chmod 1777 /dev/shm"]
162     if not relaxed:
163         ops += ["chmod 755 /etc"]
164     ops += ["exec $0 \"$@\""]
165 
166     cmdline += ["sh", "-c", " && ".join(ops)]
167 
168     return cmdline
169 
170 
171 def apivfs_cmd(root: Path) -> list[PathString]:
172     return [
173         "bwrap",
174         "--dev-bind", "/", "/",
175         "--tmpfs", root / "run",
176         "--tmpfs", root / "tmp",
177         "--bind", "/var/tmp", root / "var/tmp",
178         "--proc", root / "proc",
179         "--dev", root / "dev",
180         # APIVFS generally means chrooting is going to happen so unset TMPDIR just to be safe.
181         "--unsetenv", "TMPDIR",
182         # Make sure /etc/machine-id is not overwritten by any package manager post install scripts.
183         "--ro-bind-try", root / "etc/machine-id", root / "etc/machine-id",
184         *finalize_passwd_mounts(root),
185         "sh", "-c",
186         f"chmod 1777 {root / 'tmp'} {root / 'var/tmp'} {root / 'dev/shm'} && "
187         f"chmod 755 {root / 'run'} && "
188         # Make sure anything running in the root directory thinks it's in a container. $container can't always be
189         # accessed so we write /run/host/container-manager as well which is always accessible.
190         f"mkdir -m 755 {root}/run/host && echo mkosi >{root}/run/host/container-manager && "
191         "exec $0 \"$@\"",
192     ]
193 
194 
195 def chroot_cmd(root: Path, *, resolve: bool = False, options: Sequence[PathString] = ()) -> list[PathString]:
196     cmdline: list[PathString] = [
197         "sh", "-c",
198         f"trap 'rm -rf {root / 'work'}' EXIT && "
199         # /etc/resolv.conf can be a dangling symlink to /run/systemd/resolve/stub-resolv.conf. Bubblewrap tries to call
200         # mkdir() on each component of the path which means it will try to call
201         # mkdir(/run/systemd/resolve/stub-resolv.conf) which will fail unless /run/systemd/resolve exists already so
202         # we make sure that it already exists.
203         f"mkdir -p -m 755 {root / 'work'} {root / 'run/systemd'} {root / 'run/systemd/resolve'} && "
204         # No exec here because we need to clean up the /work directory afterwards.
205         f"$0 \"$@\"",
206         "bwrap",
207         "--dev-bind", root, "/",
208         "--setenv", "container", "mkosi",
209         "--setenv", "HOME", "/",
210         "--setenv", "PATH", "/work/scripts:/usr/bin:/usr/sbin",
211     ]
212 
213     if resolve:
214         cmdline += ["--ro-bind-try", "/etc/resolv.conf", "/etc/resolv.conf"]
215 
216     cmdline += options
217 
218     return apivfs_cmd(root) + cmdline
219
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/mkosi/sandbox.py b/mkosi/sandbox.py
--- a/mkosi/sandbox.py
+++ b/mkosi/sandbox.py
@@ -119,9 +119,16 @@
             if Path(d).exists():
                 cmdline += ["--bind", d, d]
 
-        # `Path.parents` only supports slices and negative indexing from Python 3.10 onwards.
-        # TODO: Remove list() when we depend on Python 3.10 or newer.
-        if (d := os.fspath(list(Path.cwd().parents)[-2])) not in (*dirs, "/home", "/usr", "/nix", "/tmp"):
+        if len(Path.cwd().parents) >= 2:
+            # `Path.parents` only supports slices and negative indexing from Python 3.10 onwards.
+            # TODO: Remove list() when we depend on Python 3.10 or newer.
+            d = os.fspath(list(Path.cwd().parents)[-2])
+        elif len(Path.cwd().parents) == 1:
+            d = os.fspath(Path.cwd())
+        else:
+            d = ""
+
+        if d and d not in (*dirs, "/home", "/usr", "/nix", "/tmp"):
             cmdline += ["--bind", d, d]
 
     if vartmp:

{"golden_diff": "diff --git a/mkosi/sandbox.py b/mkosi/sandbox.py\n--- a/mkosi/sandbox.py\n+++ b/mkosi/sandbox.py\n@@ -119,9 +119,16 @@\n             if Path(d).exists():\n                 cmdline += [\"--bind\", d, d]\n \n-        # `Path.parents` only supports slices and negative indexing from Python 3.10 onwards.\n-        # TODO: Remove list() when we depend on Python 3.10 or newer.\n-        if (d := os.fspath(list(Path.cwd().parents)[-2])) not in (*dirs, \"/home\", \"/usr\", \"/nix\", \"/tmp\"):\n+        if len(Path.cwd().parents) >= 2:\n+            # `Path.parents` only supports slices and negative indexing from Python 3.10 onwards.\n+            # TODO: Remove list() when we depend on Python 3.10 or newer.\n+            d = os.fspath(list(Path.cwd().parents)[-2])\n+        elif len(Path.cwd().parents) == 1:\n+            d = os.fspath(Path.cwd())\n+        else:\n+            d = \"\"\n+\n+        if d and d not in (*dirs, \"/home\", \"/usr\", \"/nix\", \"/tmp\"):\n             cmdline += [\"--bind\", d, d]\n \n     if vartmp:\n", "issue": "`mkosi qemu` crashes when `--directory` has only one path component\nStarting with version 20 (`20.1-1~bpo12+1` from Debian bookworm-backports), `mkosi qemu` seems to assume that its config directory path always has at least 2 components. This leads to crashes like the following:\r\n\r\n```\r\nroot@mkosi:~# mkdir /foo; mkosi --directory=/foo qemu\r\n[...]\r\nmke2fs 1.47.0 (5-Feb-2023)\r\nTraceback (most recent call last):\r\n  File \"/usr/lib/python3/dist-packages/mkosi/run.py\", line 162, in uncaught_exception_handler\r\n    yield\r\n  File \"/usr/lib/python3.11/contextlib.py\", line 81, in inner\r\n    return func(*args, **kwds)\r\n           ^^^^^^^^^^^^^^^^^^^\r\n  File \"/usr/lib/python3/dist-packages/mkosi/__main__.py\", line 34, in main\r\n    run_verb(args, images)\r\n  File \"/usr/lib/python3/dist-packages/mkosi/__init__.py\", line 3468, in run_verb\r\n    {\r\n  File \"/usr/lib/python3/dist-packages/mkosi/qemu.py\", line 830, in run_qemu\r\n    sandbox=config.sandbox(network=True, devices=True, relaxed=True),\r\n            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\r\n  File \"/usr/lib/python3/dist-packages/mkosi/config.py\", line 1386, in sandbox\r\n    return sandbox_cmd(\r\n           ^^^^^^^^^^^^\r\n  File \"/usr/lib/python3/dist-packages/mkosi/sandbox.py\", line 124, in sandbox_cmd\r\n    if (d := os.fspath(list(Path.cwd().parents)[-2])) not in (*dirs, \"/home\", \"/usr\", \"/nix\", \"/tmp\"):\r\n                       ~~~~~~~~~~~~~~~~~~~~~~~~^^^^\r\nIndexError: list index out of range\r\n```\r\n\r\nI ran into this problem by using mkosi inside a Vagrant VM, which mounts the project directory to `/vagrant` by default. Changing that mountpoint to `/media/vagrant` works as a workaround.\r\n\r\n\n", "before_files": [{"content": "# SPDX-License-Identifier: LGPL-2.1+\nimport enum\nimport logging\nimport os\nimport uuid\nfrom collections.abc import Sequence\nfrom pathlib import Path\nfrom typing import Optional\n\nfrom mkosi.types import PathString\nfrom mkosi.util import INVOKING_USER, flatten, one_zero\n\n\n# https://github.com/torvalds/linux/blob/master/include/uapi/linux/capability.h\nclass Capability(enum.Enum):\n    CAP_NET_ADMIN = 12\n\n\ndef have_effective_cap(capability: Capability) -> bool:\n    for line in Path(\"/proc/self/status\").read_text().splitlines():\n        if line.startswith(\"CapEff:\"):\n            hexcap = line.removeprefix(\"CapEff:\").strip()\n            break\n    else:\n        logging.warning(f\"\\\"CapEff:\\\" not found in /proc/self/status, assuming we don't have {capability}\")\n        return False\n\n    return (int(hexcap, 16) & (1 << capability.value)) != 0\n\n\ndef finalize_passwd_mounts(root: Path) -> list[PathString]:\n    \"\"\"\n    If passwd or a related file exists in the apivfs directory, bind mount it over the host files while we\n    run the command, to make sure that the command we run uses user/group information from the apivfs\n    directory instead of from the host.\n    \"\"\"\n    options: list[PathString] = []\n\n    for f in (\"passwd\", \"group\", \"shadow\", \"gshadow\"):\n        options += [\"--ro-bind-try\", root / \"etc\" / f, f\"/etc/{f}\"]\n\n    return options\n\n\ndef finalize_crypto_mounts(tools: Path = Path(\"/\")) -> list[PathString]:\n    mounts = [\n        (tools / subdir, Path(\"/\") / subdir)\n        for subdir in (\n            Path(\"etc/pki\"),\n            Path(\"etc/ssl\"),\n            Path(\"etc/crypto-policies\"),\n            Path(\"etc/ca-certificates\"),\n            Path(\"etc/pacman.d/gnupg\"),\n            Path(\"var/lib/ca-certificates\"),\n        )\n        if (tools / subdir).exists()\n    ]\n\n    return flatten(\n        [\"--ro-bind\", src, target]\n        for src, target\n        in sorted(set(mounts), key=lambda s: s[1])\n    )\n\n\ndef sandbox_cmd(\n    *,\n    network: bool = False,\n    devices: bool = False,\n    scripts: Optional[Path] = None,\n    tools: Path = Path(\"/\"),\n    relaxed: bool = False,\n    options: Sequence[PathString] = (),\n) -> list[PathString]:\n    cmdline: list[PathString] = []\n\n    if not relaxed:\n        # We want to use an empty subdirectory in the host's /var/tmp as the sandbox's /var/tmp. To make sure it only\n        # gets created when we run the sandboxed command and cleaned up when the sandboxed command exits, we create it\n        # using shell.\n        vartmp = f\"/var/tmp/mkosi-var-tmp-{uuid.uuid4().hex[:16]}\"\n        cmdline += [\"sh\", \"-c\", f\"trap 'rm -rf {vartmp}' EXIT && mkdir --mode 1777 {vartmp} && $0 \\\"$@\\\"\"]\n    else:\n        vartmp = None\n\n    cmdline += [\n        \"bwrap\",\n        \"--ro-bind\", tools / \"usr\", \"/usr\",\n        *([\"--unshare-net\"] if not network and have_effective_cap(Capability.CAP_NET_ADMIN) else []),\n        \"--die-with-parent\",\n        \"--proc\", \"/proc\",\n        \"--setenv\", \"SYSTEMD_OFFLINE\", one_zero(network),\n    ]\n\n    if relaxed:\n        cmdline += [\"--bind\", \"/tmp\", \"/tmp\"]\n    else:\n        cmdline += [\n            \"--tmpfs\", \"/tmp\",\n            \"--unshare-ipc\",\n        ]\n\n    if (tools / \"nix/store\").exists():\n        cmdline += [\"--bind\", tools / \"nix/store\", \"/nix/store\"]\n\n    if devices or relaxed:\n        cmdline += [\n            \"--bind\", \"/sys\", \"/sys\",\n            \"--bind\", \"/run\", \"/run\",\n            \"--dev-bind\", \"/dev\", \"/dev\",\n        ]\n    else:\n        cmdline += [\"--dev\", \"/dev\"]\n\n    if relaxed:\n        dirs = (\"/etc\", \"/opt\", \"/srv\", \"/media\", \"/mnt\", \"/var\", os.fspath(INVOKING_USER.home()))\n\n        for d in dirs:\n            if Path(d).exists():\n                cmdline += [\"--bind\", d, d]\n\n        # `Path.parents` only supports slices and negative indexing from Python 3.10 onwards.\n        # TODO: Remove list() when we depend on Python 3.10 or newer.\n        if (d := os.fspath(list(Path.cwd().parents)[-2])) not in (*dirs, \"/home\", \"/usr\", \"/nix\", \"/tmp\"):\n            cmdline += [\"--bind\", d, d]\n\n    if vartmp:\n        cmdline += [\"--bind\", vartmp, \"/var/tmp\"]\n\n    for d in (\"bin\", \"sbin\", \"lib\", \"lib32\", \"lib64\"):\n        if (p := tools / d).is_symlink():\n            cmdline += [\"--symlink\", p.readlink(), Path(\"/\") / p.relative_to(tools)]\n\n    path = \"/usr/bin:/usr/sbin\" if tools != Path(\"/\") else os.environ[\"PATH\"]\n\n    cmdline += [\n        \"--setenv\", \"PATH\", f\"{scripts or ''}:{path}\",\n        *options,\n    ]\n\n    if not relaxed:\n        cmdline += [\"--symlink\", \"../proc/self/mounts\", \"/etc/mtab\"]\n\n    # If we're using /usr from a tools tree, we have to use /etc/alternatives from the tools tree as well if it\n    # exists since that points directly back to /usr. Apply this after the options so the caller can mount\n    # something else to /etc without overriding this mount. In relaxed mode, we only do this if /etc/alternatives\n    # already exists on the host as otherwise we'd modify the host's /etc by creating the mountpoint ourselves (or\n    # fail when trying to create it).\n    if (tools / \"etc/alternatives\").exists() and (not relaxed or Path(\"/etc/alternatives\").exists()):\n        cmdline += [\"--ro-bind\", tools / \"etc/alternatives\", \"/etc/alternatives\"]\n\n    if scripts:\n        cmdline += [\"--ro-bind\", scripts, scripts]\n\n    if network and not relaxed:\n        cmdline += [\"--bind\", \"/etc/resolv.conf\", \"/etc/resolv.conf\"]\n\n    # bubblewrap creates everything with a restricted mode so relax stuff as needed.\n    ops = []\n    if not devices:\n        ops += [\"chmod 1777 /dev/shm\"]\n    if not relaxed:\n        ops += [\"chmod 755 /etc\"]\n    ops += [\"exec $0 \\\"$@\\\"\"]\n\n    cmdline += [\"sh\", \"-c\", \" && \".join(ops)]\n\n    return cmdline\n\n\ndef apivfs_cmd(root: Path) -> list[PathString]:\n    return [\n        \"bwrap\",\n        \"--dev-bind\", \"/\", \"/\",\n        \"--tmpfs\", root / \"run\",\n        \"--tmpfs\", root / \"tmp\",\n        \"--bind\", \"/var/tmp\", root / \"var/tmp\",\n        \"--proc\", root / \"proc\",\n        \"--dev\", root / \"dev\",\n        # APIVFS generally means chrooting is going to happen so unset TMPDIR just to be safe.\n        \"--unsetenv\", \"TMPDIR\",\n        # Make sure /etc/machine-id is not overwritten by any package manager post install scripts.\n        \"--ro-bind-try\", root / \"etc/machine-id\", root / \"etc/machine-id\",\n        *finalize_passwd_mounts(root),\n        \"sh\", \"-c\",\n        f\"chmod 1777 {root / 'tmp'} {root / 'var/tmp'} {root / 'dev/shm'} && \"\n        f\"chmod 755 {root / 'run'} && \"\n        # Make sure anything running in the root directory thinks it's in a container. $container can't always be\n        # accessed so we write /run/host/container-manager as well which is always accessible.\n        f\"mkdir -m 755 {root}/run/host && echo mkosi >{root}/run/host/container-manager && \"\n        \"exec $0 \\\"$@\\\"\",\n    ]\n\n\ndef chroot_cmd(root: Path, *, resolve: bool = False, options: Sequence[PathString] = ()) -> list[PathString]:\n    cmdline: list[PathString] = [\n        \"sh\", \"-c\",\n        f\"trap 'rm -rf {root / 'work'}' EXIT && \"\n        # /etc/resolv.conf can be a dangling symlink to /run/systemd/resolve/stub-resolv.conf. Bubblewrap tries to call\n        # mkdir() on each component of the path which means it will try to call\n        # mkdir(/run/systemd/resolve/stub-resolv.conf) which will fail unless /run/systemd/resolve exists already so\n        # we make sure that it already exists.\n        f\"mkdir -p -m 755 {root / 'work'} {root / 'run/systemd'} {root / 'run/systemd/resolve'} && \"\n        # No exec here because we need to clean up the /work directory afterwards.\n        f\"$0 \\\"$@\\\"\",\n        \"bwrap\",\n        \"--dev-bind\", root, \"/\",\n        \"--setenv\", \"container\", \"mkosi\",\n        \"--setenv\", \"HOME\", \"/\",\n        \"--setenv\", \"PATH\", \"/work/scripts:/usr/bin:/usr/sbin\",\n    ]\n\n    if resolve:\n        cmdline += [\"--ro-bind-try\", \"/etc/resolv.conf\", \"/etc/resolv.conf\"]\n\n    cmdline += options\n\n    return apivfs_cmd(root) + cmdline\n", "path": "mkosi/sandbox.py"}], "after_files": [{"content": "# SPDX-License-Identifier: LGPL-2.1+\nimport enum\nimport logging\nimport os\nimport uuid\nfrom collections.abc import Sequence\nfrom pathlib import Path\nfrom typing import Optional\n\nfrom mkosi.types import PathString\nfrom mkosi.util import INVOKING_USER, flatten, one_zero\n\n\n# https://github.com/torvalds/linux/blob/master/include/uapi/linux/capability.h\nclass Capability(enum.Enum):\n    CAP_NET_ADMIN = 12\n\n\ndef have_effective_cap(capability: Capability) -> bool:\n    for line in Path(\"/proc/self/status\").read_text().splitlines():\n        if line.startswith(\"CapEff:\"):\n            hexcap = line.removeprefix(\"CapEff:\").strip()\n            break\n    else:\n        logging.warning(f\"\\\"CapEff:\\\" not found in /proc/self/status, assuming we don't have {capability}\")\n        return False\n\n    return (int(hexcap, 16) & (1 << capability.value)) != 0\n\n\ndef finalize_passwd_mounts(root: Path) -> list[PathString]:\n    \"\"\"\n    If passwd or a related file exists in the apivfs directory, bind mount it over the host files while we\n    run the command, to make sure that the command we run uses user/group information from the apivfs\n    directory instead of from the host.\n    \"\"\"\n    options: list[PathString] = []\n\n    for f in (\"passwd\", \"group\", \"shadow\", \"gshadow\"):\n        options += [\"--ro-bind-try\", root / \"etc\" / f, f\"/etc/{f}\"]\n\n    return options\n\n\ndef finalize_crypto_mounts(tools: Path = Path(\"/\")) -> list[PathString]:\n    mounts = [\n        (tools / subdir, Path(\"/\") / subdir)\n        for subdir in (\n            Path(\"etc/pki\"),\n            Path(\"etc/ssl\"),\n            Path(\"etc/crypto-policies\"),\n            Path(\"etc/ca-certificates\"),\n            Path(\"etc/pacman.d/gnupg\"),\n            Path(\"var/lib/ca-certificates\"),\n        )\n        if (tools / subdir).exists()\n    ]\n\n    return flatten(\n        [\"--ro-bind\", src, target]\n        for src, target\n        in sorted(set(mounts), key=lambda s: s[1])\n    )\n\n\ndef sandbox_cmd(\n    *,\n    network: bool = False,\n    devices: bool = False,\n    scripts: Optional[Path] = None,\n    tools: Path = Path(\"/\"),\n    relaxed: bool = False,\n    options: Sequence[PathString] = (),\n) -> list[PathString]:\n    cmdline: list[PathString] = []\n\n    if not relaxed:\n        # We want to use an empty subdirectory in the host's /var/tmp as the sandbox's /var/tmp. To make sure it only\n        # gets created when we run the sandboxed command and cleaned up when the sandboxed command exits, we create it\n        # using shell.\n        vartmp = f\"/var/tmp/mkosi-var-tmp-{uuid.uuid4().hex[:16]}\"\n        cmdline += [\"sh\", \"-c\", f\"trap 'rm -rf {vartmp}' EXIT && mkdir --mode 1777 {vartmp} && $0 \\\"$@\\\"\"]\n    else:\n        vartmp = None\n\n    cmdline += [\n        \"bwrap\",\n        \"--ro-bind\", tools / \"usr\", \"/usr\",\n        *([\"--unshare-net\"] if not network and have_effective_cap(Capability.CAP_NET_ADMIN) else []),\n        \"--die-with-parent\",\n        \"--proc\", \"/proc\",\n        \"--setenv\", \"SYSTEMD_OFFLINE\", one_zero(network),\n    ]\n\n    if relaxed:\n        cmdline += [\"--bind\", \"/tmp\", \"/tmp\"]\n    else:\n        cmdline += [\n            \"--tmpfs\", \"/tmp\",\n            \"--unshare-ipc\",\n        ]\n\n    if (tools / \"nix/store\").exists():\n        cmdline += [\"--bind\", tools / \"nix/store\", \"/nix/store\"]\n\n    if devices or relaxed:\n        cmdline += [\n            \"--bind\", \"/sys\", \"/sys\",\n            \"--bind\", \"/run\", \"/run\",\n            \"--dev-bind\", \"/dev\", \"/dev\",\n        ]\n    else:\n        cmdline += [\"--dev\", \"/dev\"]\n\n    if relaxed:\n        dirs = (\"/etc\", \"/opt\", \"/srv\", \"/media\", \"/mnt\", \"/var\", os.fspath(INVOKING_USER.home()))\n\n        for d in dirs:\n            if Path(d).exists():\n                cmdline += [\"--bind\", d, d]\n\n        if len(Path.cwd().parents) >= 2:\n            # `Path.parents` only supports slices and negative indexing from Python 3.10 onwards.\n            # TODO: Remove list() when we depend on Python 3.10 or newer.\n            d = os.fspath(list(Path.cwd().parents)[-2])\n        elif len(Path.cwd().parents) == 1:\n            d = os.fspath(Path.cwd())\n        else:\n            d = \"\"\n\n        if d and d not in (*dirs, \"/home\", \"/usr\", \"/nix\", \"/tmp\"):\n            cmdline += [\"--bind\", d, d]\n\n    if vartmp:\n        cmdline += [\"--bind\", vartmp, \"/var/tmp\"]\n\n    for d in (\"bin\", \"sbin\", \"lib\", \"lib32\", \"lib64\"):\n        if (p := tools / d).is_symlink():\n            cmdline += [\"--symlink\", p.readlink(), Path(\"/\") / p.relative_to(tools)]\n\n    path = \"/usr/bin:/usr/sbin\" if tools != Path(\"/\") else os.environ[\"PATH\"]\n\n    cmdline += [\n        \"--setenv\", \"PATH\", f\"{scripts or ''}:{path}\",\n        *options,\n    ]\n\n    if not relaxed:\n        cmdline += [\"--symlink\", \"../proc/self/mounts\", \"/etc/mtab\"]\n\n    # If we're using /usr from a tools tree, we have to use /etc/alternatives from the tools tree as well if it\n    # exists since that points directly back to /usr. Apply this after the options so the caller can mount\n    # something else to /etc without overriding this mount. In relaxed mode, we only do this if /etc/alternatives\n    # already exists on the host as otherwise we'd modify the host's /etc by creating the mountpoint ourselves (or\n    # fail when trying to create it).\n    if (tools / \"etc/alternatives\").exists() and (not relaxed or Path(\"/etc/alternatives\").exists()):\n        cmdline += [\"--ro-bind\", tools / \"etc/alternatives\", \"/etc/alternatives\"]\n\n    if scripts:\n        cmdline += [\"--ro-bind\", scripts, scripts]\n\n    if network and not relaxed:\n        cmdline += [\"--bind\", \"/etc/resolv.conf\", \"/etc/resolv.conf\"]\n\n    # bubblewrap creates everything with a restricted mode so relax stuff as needed.\n    ops = []\n    if not devices:\n        ops += [\"chmod 1777 /dev/shm\"]\n    if not relaxed:\n        ops += [\"chmod 755 /etc\"]\n    ops += [\"exec $0 \\\"$@\\\"\"]\n\n    cmdline += [\"sh\", \"-c\", \" && \".join(ops)]\n\n    return cmdline\n\n\ndef apivfs_cmd(root: Path) -> list[PathString]:\n    return [\n        \"bwrap\",\n        \"--dev-bind\", \"/\", \"/\",\n        \"--tmpfs\", root / \"run\",\n        \"--tmpfs\", root / \"tmp\",\n        \"--bind\", \"/var/tmp\", root / \"var/tmp\",\n        \"--proc\", root / \"proc\",\n        \"--dev\", root / \"dev\",\n        # APIVFS generally means chrooting is going to happen so unset TMPDIR just to be safe.\n        \"--unsetenv\", \"TMPDIR\",\n        # Make sure /etc/machine-id is not overwritten by any package manager post install scripts.\n        \"--ro-bind-try\", root / \"etc/machine-id\", root / \"etc/machine-id\",\n        *finalize_passwd_mounts(root),\n        \"sh\", \"-c\",\n        f\"chmod 1777 {root / 'tmp'} {root / 'var/tmp'} {root / 'dev/shm'} && \"\n        f\"chmod 755 {root / 'run'} && \"\n        # Make sure anything running in the root directory thinks it's in a container. $container can't always be\n        # accessed so we write /run/host/container-manager as well which is always accessible.\n        f\"mkdir -m 755 {root}/run/host && echo mkosi >{root}/run/host/container-manager && \"\n        \"exec $0 \\\"$@\\\"\",\n    ]\n\n\ndef chroot_cmd(root: Path, *, resolve: bool = False, options: Sequence[PathString] = ()) -> list[PathString]:\n    cmdline: list[PathString] = [\n        \"sh\", \"-c\",\n        f\"trap 'rm -rf {root / 'work'}' EXIT && \"\n        # /etc/resolv.conf can be a dangling symlink to /run/systemd/resolve/stub-resolv.conf. Bubblewrap tries to call\n        # mkdir() on each component of the path which means it will try to call\n        # mkdir(/run/systemd/resolve/stub-resolv.conf) which will fail unless /run/systemd/resolve exists already so\n        # we make sure that it already exists.\n        f\"mkdir -p -m 755 {root / 'work'} {root / 'run/systemd'} {root / 'run/systemd/resolve'} && \"\n        # No exec here because we need to clean up the /work directory afterwards.\n        f\"$0 \\\"$@\\\"\",\n        \"bwrap\",\n        \"--dev-bind\", root, \"/\",\n        \"--setenv\", \"container\", \"mkosi\",\n        \"--setenv\", \"HOME\", \"/\",\n        \"--setenv\", \"PATH\", \"/work/scripts:/usr/bin:/usr/sbin\",\n    ]\n\n    if resolve:\n        cmdline += [\"--ro-bind-try\", \"/etc/resolv.conf\", \"/etc/resolv.conf\"]\n\n    cmdline += options\n\n    return apivfs_cmd(root) + cmdline\n", "path": "mkosi/sandbox.py"}]}