diff --git a/mkosi/__init__.py b/mkosi/__init__.py index b9f99aafb..9c6d92a41 100644 --- a/mkosi/__init__.py +++ b/mkosi/__init__.py @@ -103,7 +103,7 @@ chroot_cmd, chroot_options, finalize_interpreter, - finalize_passwd_mounts, + finalize_passwd_symlinks, fork_and_wait, run, workdir, @@ -2822,9 +2822,8 @@ def run_tmpfiles(context: Context) -> None: options=[ "--bind", context.root, "/buildroot", # 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), + # passwd so we have to symlink the image's passwd to make ACL parsing work. + *finalize_passwd_symlinks("/buildroot"), # Sometimes directories are configured to be owned by root in tmpfiles snippets # so we want to make sure those chown()'s succeed by making ourselves the root # user so that the root user exists. diff --git a/mkosi/archive.py b/mkosi/archive.py index b735955bf..5b5b6ea91 100644 --- a/mkosi/archive.py +++ b/mkosi/archive.py @@ -6,7 +6,7 @@ from typing import Optional from mkosi.log import log_step -from mkosi.run import SandboxProtocol, finalize_passwd_mounts, nosandbox, run, workdir +from mkosi.run import SandboxProtocol, finalize_passwd_symlinks, nosandbox, run, workdir from mkosi.sandbox import umask from mkosi.types import PathString from mkosi.util import chdir @@ -51,7 +51,7 @@ def make_tar(src: Path, dst: Path, *, sandbox: SandboxProtocol = nosandbox) -> N # Make sure tar uses user/group information from the root directory instead of the host. sandbox=sandbox( binary="tar", - options=["--ro-bind", src, workdir(src), *finalize_passwd_mounts(src)], + options=["--ro-bind", src, workdir(src), *finalize_passwd_symlinks(workdir(src))], ), ) # fmt: skip @@ -98,7 +98,7 @@ def extract_tar( options=[ "--ro-bind", src, workdir(src), "--bind", dst, workdir(dst), - *finalize_passwd_mounts(dst), + *finalize_passwd_symlinks(workdir(dst)), ], ), ) # fmt: skip @@ -136,6 +136,6 @@ def make_cpio( stdout=f, sandbox=sandbox( binary="cpio", - options=["--ro-bind", src, workdir(src), *finalize_passwd_mounts(src)], + options=["--ro-bind", src, workdir(src), *finalize_passwd_symlinks(workdir(src))], ), ) # fmt: skip diff --git a/mkosi/installer/__init__.py b/mkosi/installer/__init__.py index 832f11f22..fc9713420 100644 --- a/mkosi/installer/__init__.py +++ b/mkosi/installer/__init__.py @@ -7,7 +7,7 @@ from mkosi.config import Config, ConfigFeature, OutputFormat from mkosi.context import Context from mkosi.mounts import finalize_crypto_mounts -from mkosi.run import apivfs_options, finalize_interpreter, finalize_passwd_mounts, find_binary +from mkosi.run import apivfs_options, finalize_interpreter, finalize_passwd_symlinks, find_binary from mkosi.tree import rmtree from mkosi.types import PathString from mkosi.util import flatten, startswith @@ -109,10 +109,9 @@ def options(cls, *, root: PathString, apivfs: bool = True) -> list[PathString]: "--suppress-chown", # Make sure /etc/machine-id is not overwritten by any package manager post install scripts. "--ro-bind-try", Path(root) / "etc/machine-id", "/buildroot/etc/machine-id", - # If we're already in the sandbox, we want to pick up use the passwd files from /buildroot since - # the original root won't be available anymore. If we're not in the sandbox yet, we want to pick - # up the passwd files from the original root. - *finalize_passwd_mounts(root), + # Some package managers (e.g. dpkg) read from the host's /etc/passwd instead of the buildroot's + # /etc/passwd so we symlink /etc/passwd from the buildroot to make sure it gets used. + *(finalize_passwd_symlinks("/buildroot") if apivfs else []), ] # fmt: skip @classmethod diff --git a/mkosi/resources/man/mkosi-sandbox.1.md b/mkosi/resources/man/mkosi-sandbox.1.md index 691f170fc..b171e250a 100644 --- a/mkosi/resources/man/mkosi-sandbox.1.md +++ b/mkosi/resources/man/mkosi-sandbox.1.md @@ -54,7 +54,9 @@ host system. : Like `--bind-try`, but does a recursive readonly bind mount. `--symlink SRC DST` -: Creates a symlink at `DST` in the sandbox pointing to `SRC`. +: Creates a symlink at `DST` in the sandbox pointing to `SRC`. If `DST` already + exists and is a file or symlink, a temporary symlink is created and mounted on + top of `DST`. `--write DATA DST` : Writes the string from `DATA` to `DST` in the sandbox. diff --git a/mkosi/run.py b/mkosi/run.py index 8c507655d..b777fffb7 100644 --- a/mkosi/run.py +++ b/mkosi/run.py @@ -415,15 +415,14 @@ def workdir(path: Path, sandbox: Optional[SandboxProtocol] = None) -> str: return joinpath(subdir, str(path)) -def finalize_passwd_mounts(root: PathString) -> list[PathString]: +def finalize_passwd_symlinks(root: PathString) -> list[PathString]: """ 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. """ return flatten( - ("--ro-bind-try", Path(root) / "etc" / f, f"/etc/{f}") - for f in ("passwd", "group", "shadow", "gshadow") + ("--symlink", Path(root) / "etc" / f, f"/etc/{f}") for f in ("passwd", "group", "shadow", "gshadow") ) diff --git a/mkosi/sandbox.py b/mkosi/sandbox.py index 0cb86378e..249f3332c 100755 --- a/mkosi/sandbox.py +++ b/mkosi/sandbox.py @@ -560,12 +560,19 @@ def __init__(self, src: str, dst: str) -> None: def execute(self, oldroot: str, newroot: str) -> None: dst = joinpath(newroot, self.dst) try: - os.symlink(self.src, dst) + return os.symlink(self.src, dst) except FileExistsError: - if os.readlink(dst) == self.src: + if os.path.islink(dst) and os.readlink(dst) == self.src: return - raise + if os.path.isdir(dst): + raise + + # If the target already exists and is not a directory, create the symlink somewhere else and mount + # it over the existing file or symlink. + os.symlink(self.src, "/symlink") + mount_rbind("/symlink", dst) + os.unlink("/symlink") class WriteOperation(FSOperation):