refactor(left4me-overlay): move uid translation to script-sandbox build

left4me-script-sandbox now pre-creates an idmapped bind staging path
(--map-users=<left4me_uid>:<sandbox_uid>:1) and points the sandbox's
BindPaths at that staging instead of the raw overlay dir. Writes from
inside the sandbox (uid l4d2-sandbox) land on disk as left4me, so all
overlay content is uniformly left4me-owned end-to-end.

left4me-overlay loses ~165 lines of idmap-on-mount logic: the per-
lowerdir stat + idmap-bind setup, the bind-umount loop in teardown,
the uid lookup helpers, the _is_mountpoint /proc/self/mountinfo parser,
and the LEFT4ME_TEST_* env-var stubs. It's back to a simple "validate
lowerdirs, mount overlay" shape; gameserver mount path no longer needs
to know about producer-side ownership decisions.

Verified on kernel 6.12 that the kernel idmap propagates through
systemd-run's plain re-bind of the staging path. Tests dropped 4
idmap-on-mount specs and one deploy-artifact regression check; added
test_script_sandbox_uses_idmap_staging to pin the new staging path
+ map flags + trap cleanup.

The post-build world-read chmod kludge in the sandbox is also dropped:
the web app reads overlay files via its primary uid (left4me).

Existing overlays on the test server are sandbox-owned from prior runs
and need a one-shot `chown -R left4me:left4me /var/lib/left4me/overlays`
during deploy. New overlays produced by the refactored sandbox are
left4me-owned from creation.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
mwiegand 2026-05-15 01:20:39 +02:00
parent bc25d423aa
commit 48381089d3
No known key found for this signature in database
4 changed files with 51 additions and 377 deletions

View file

@ -29,7 +29,6 @@ shell-quoted) and exit 0 instead of execv. Used by tests.
"""
import os
import pwd
import re
import shlex
import shutil
@ -55,74 +54,6 @@ def die(msg: str) -> None:
sys.exit(1)
def _is_mountpoint(
path: str | Path,
mountinfo_path: str = "/proc/self/mountinfo",
) -> bool:
"""Reliable mount-point check that handles same-fs bind mounts.
`os.path.ismount()` compares `st_dev` of the path against its parent;
bind mounts on the same underlying filesystem share `st_dev` with their
parent, so `os.path.ismount()` returns False for them. The idmap binds
we install on `runtime/<n>/idmap/<basename>` are exactly that case.
Read /proc/self/mountinfo (field 5 is the mount point) for a check
that works regardless of mount type. The override is for tests only.
"""
abs_path = os.fspath(Path(path).resolve())
try:
with open(mountinfo_path, "r", encoding="utf-8") as f:
for line in f:
fields = line.split()
if len(fields) >= 5 and fields[4] == abs_path:
return True
except OSError:
pass
return False
def _lookup_uid(username: str) -> tuple[int, int]:
"""Return (uid, gid) for *username*, dying with a clear message if missing."""
try:
entry = pwd.getpwnam(username)
except KeyError:
die(
f"required system user {username!r} does not exist; "
"this is a deploy misconfiguration"
)
return entry.pw_uid, entry.pw_gid
def _get_user_ids() -> tuple[int, int, int, int]:
"""Return (sandbox_uid, sandbox_gid, left4me_uid, left4me_gid).
In normal operation, looks up the real system users. In PRINT_ONLY
(test) mode the env vars LEFT4ME_TEST_SANDBOX_UID/LEFT4ME_TEST_SANDBOX_GID/
LEFT4ME_TEST_LEFT4ME_UID/LEFT4ME_TEST_LEFT4ME_GID may be used to inject
synthetic uids so tests can run without root and without real system
users present. The stubs are intentionally ignored outside PRINT_ONLY
mode so that a misconfigured systemd unit override cannot influence the
real uid mapping.
"""
if os.environ.get("LEFT4ME_OVERLAY_PRINT_ONLY") == "1":
sandbox_uid_env = os.environ.get("LEFT4ME_TEST_SANDBOX_UID")
sandbox_gid_env = os.environ.get("LEFT4ME_TEST_SANDBOX_GID")
left4me_uid_env = os.environ.get("LEFT4ME_TEST_LEFT4ME_UID")
left4me_gid_env = os.environ.get("LEFT4ME_TEST_LEFT4ME_GID")
if all(v is not None for v in (sandbox_uid_env, sandbox_gid_env,
left4me_uid_env, left4me_gid_env)):
return (
int(sandbox_uid_env), # type: ignore[arg-type]
int(sandbox_gid_env), # type: ignore[arg-type]
int(left4me_uid_env), # type: ignore[arg-type]
int(left4me_gid_env), # type: ignore[arg-type]
)
sandbox_uid, sandbox_gid = _lookup_uid("l4d2-sandbox")
left4me_uid, left4me_gid = _lookup_uid("left4me")
return sandbox_uid, sandbox_gid, left4me_uid, left4me_gid
def root() -> Path:
return Path(os.environ.get("LEFT4ME_ROOT") or DEFAULT_ROOT)
@ -242,79 +173,7 @@ def cmd_mount(name: str) -> None:
assert_no_fuse_xattrs(upper)
# Resolve user ids now (fails fast on deploy misconfiguration).
sandbox_uid, sandbox_gid, left4me_uid, left4me_gid = _get_user_ids()
# Build the final lowerdir list, substituting idmap bind-mount paths for
# any lowerdir owned by l4d2-sandbox. An idmap bind mount makes the kernel
# see the l4d2-sandbox-owned tree as if it were owned by left4me, so that
# overlayfs copy-up produces left4me-owned upperdir entries.
idmap_dir = runtime_name_dir / "idmap"
final_lowerdirs: list[str] = []
bind_argvs: list[list[str]] = []
seen_idmap_targets: dict[Path, str] = {}
for lowerdir in canonical_lowerdirs:
try:
st = os.stat(lowerdir)
except OSError as exc:
die(f"failed to stat lowerdir {shlex.quote(lowerdir)}: {exc}")
if st.st_uid == sandbox_uid:
# This lowerdir needs idmap remapping.
# Include the parent dirname to avoid basename collisions between
# lowerdirs from different allowlist roots (e.g. overlays/foo and
# workshop_cache/foo would otherwise map to the same idmap target).
p = Path(lowerdir)
lowerdir_basename = f"{p.parent.name}_{p.name}"
idmap_target = idmap_dir / lowerdir_basename
# Belt-and-braces: detect if two different lowerdirs would collide
# on the same idmap target after the parent+name derivation.
if idmap_target in seen_idmap_targets:
die(
f"idmap target collision: lowerdirs {shlex.quote(seen_idmap_targets[idmap_target])}"
f" and {shlex.quote(lowerdir)} both map to {idmap_target}"
)
seen_idmap_targets[idmap_target] = lowerdir
if os.environ.get("LEFT4ME_OVERLAY_PRINT_ONLY") != "1":
idmap_dir.mkdir(mode=0o700, exist_ok=True)
idmap_target.mkdir(mode=0o700, exist_ok=True)
if not _is_mountpoint(idmap_target) or \
os.environ.get("LEFT4ME_OVERLAY_PRINT_ONLY") == "1":
# --map-users / --map-groups argument format:
# <on-disk-uid>:<in-mount-uid>:<count>
# The util-linux man page calls these <inner>:<outer>, which is
# misleading. Empirically (verified on left4.me, kernel 6.12,
# ext4) the FIRST number is the on-disk uid and the SECOND is
# the uid exposed inside the mount. Don't swap them.
bind_argv = [
MOUNT_BIN,
"--bind",
f"--map-users={sandbox_uid}:{left4me_uid}:1",
f"--map-groups={sandbox_gid}:{left4me_gid}:1",
lowerdir,
str(idmap_target),
]
bind_argvs.append(bind_argv)
final_lowerdirs.append(str(idmap_target))
else:
final_lowerdirs.append(lowerdir)
print_only = os.environ.get("LEFT4ME_OVERLAY_PRINT_ONLY") == "1"
if print_only:
# Emit each bind-mount argv first, then fall through to the overlay argv.
for bind_argv in bind_argvs:
_print_argv(bind_argv)
else:
# Actually exec each bind mount before the overlay mount.
for bind_argv in bind_argvs:
subprocess.run(bind_argv, check=True)
options = f"lowerdir={':'.join(final_lowerdirs)},upperdir={upper},workdir={work}"
options = f"lowerdir={':'.join(canonical_lowerdirs)},upperdir={upper},workdir={work}"
argv = [
MOUNT_BIN,
"-t", "overlay",
@ -338,20 +197,8 @@ def cmd_umount(name: str) -> None:
str(merged_path.resolve(strict=True) if merged_path.exists() else merged_path),
]
# Collect idmap bind-umount argvs: one per direct subdir of runtime/<name>/idmap/.
idmap_dir = runtime_name_dir / "idmap"
bind_umount_argvs: list[list[str]] = []
if idmap_dir.is_dir():
for entry in sorted(idmap_dir.iterdir()):
if entry.is_dir():
bind_umount_argvs.append([UMOUNT_BIN, str(entry)])
# PRINT_ONLY: emit the overlay umount argv, then each bind-umount argv, then exit.
# Order matches real execution (overlay first, then idmap binds underneath).
if os.environ.get("LEFT4ME_OVERLAY_PRINT_ONLY") == "1":
_print_argv(overlay_umount_argv)
for bind_umount_argv in bind_umount_argvs:
_print_argv(bind_umount_argv)
sys.exit(0)
if merged_path.exists():
@ -382,16 +229,6 @@ def cmd_umount(name: str) -> None:
if work_inner.exists():
shutil.rmtree(work_inner)
# Unwind idmap bind mounts, then remove the idmap directory. Each bind
# is only umounted if it is still a mountpoint (idempotent across partial
# teardowns). _is_mountpoint reads /proc/self/mountinfo because
# os.path.ismount misses same-fs bind mounts.
for bind_umount_argv in bind_umount_argvs:
target = Path(bind_umount_argv[-1])
if _is_mountpoint(target):
subprocess.run(bind_umount_argv, check=True)
shutil.rmtree(idmap_dir, ignore_errors=True)
def main(argv: list[str]) -> None:
if len(argv) != 3 or argv[1] not in ("mount", "umount"):

View file

@ -34,13 +34,36 @@ if [[ "${LEFT4ME_SCRIPT_SANDBOX_DRY_RUN:-}" == "1" ]]; then
exit 0
fi
# Make sure the sandbox UID owns the overlay dir so the script can write there.
# Idempotent: a no-op when the dir is already l4d2-sandbox-owned (re-run case),
# and corrects the ownership the first time the dir was created by the web app
# under the left4me UID. World-readable so the gameserver process (left4me)
# can read the overlay contents via the kernel-overlayfs lowerdir at runtime.
chown -R l4d2-sandbox:l4d2-sandbox "$OVERLAY_DIR"
chmod 0755 "$OVERLAY_DIR"
# Pre-create an idmapped bind of the overlay dir, then point the sandbox's
# BindPaths at that staging path. The bind translates the sandbox's writing
# uid (l4d2-sandbox) back to left4me on disk, so all overlay content
# (script-built and workshop) is uniformly left4me-owned. Map direction:
# `--map-users=<disk_uid>:<mount_uid>:1` with disk=left4me, mount=sandbox —
# a process inside the bind with uid sandbox sees its uid as itself, and
# writes get translated to disk-uid left4me. Verified on kernel 6.12 that
# idmap propagates through systemd-run's plain re-bind of the staging path.
LEFT4ME_UID=$(id -u left4me)
LEFT4ME_GID=$(id -g left4me)
SANDBOX_UID=$(id -u l4d2-sandbox)
SANDBOX_GID=$(id -g l4d2-sandbox)
STAGING=/var/lib/left4me/tmp/sandbox-idmap-${OVERLAY_ID}
# trap fires even on errors / signals so the staging bind doesn't outlive
# this invocation. Idempotent if the staging is already gone.
cleanup_staging() {
umount "$STAGING" 2>/dev/null || true
rmdir "$STAGING" 2>/dev/null || true
}
trap cleanup_staging EXIT
# A leftover staging mount from a SIGKILLed prior run can be reset by
# umounting first, then re-binding fresh on the same path.
umount "$STAGING" 2>/dev/null || true
mkdir -p "$STAGING"
mount --bind \
--map-users="${LEFT4ME_UID}:${SANDBOX_UID}:1" \
--map-groups="${LEFT4ME_GID}:${SANDBOX_GID}:1" \
"$OVERLAY_DIR" "$STAGING"
SCRIPT_RC=0
systemd-run --quiet --collect --wait --pipe \
@ -64,19 +87,11 @@ systemd-run --quiet --collect --wait --pipe \
-p IPAddressDeny="127.0.0.0/8 ::1/128 169.254.0.0/16 fe80::/10 224.0.0.0/4 ff00::/8 10.0.0.0/8 172.16.0.0/12 192.168.0.0/16 100.64.0.0/10 fc00::/7" \
-p TemporaryFileSystem="/etc /var/lib" \
-p BindReadOnlyPaths="/etc/left4me/sandbox-resolv.conf:/etc/resolv.conf /etc/ssl /etc/ca-certificates /etc/nsswitch.conf /etc/alternatives ${SCRIPT}:/script.sh" \
-p BindPaths="${OVERLAY_DIR}:/overlay" \
-p BindPaths="${STAGING}:/overlay" \
-p WorkingDirectory=/overlay \
-p Environment="HOME=/tmp PATH=/usr/bin:/usr/sbin OVERLAY=/overlay" \
-p MemoryMax=4G -p MemorySwapMax=0 -p TasksMax=512 \
-p CPUQuota=200% -p RuntimeMaxSec=3600 \
-- /bin/bash /script.sh || SCRIPT_RC=$?
# Normalize perms so the web service (left4me uid) can read overlay files
# directly via Python open() — needed by the file tree's download endpoint.
# UMask=0022 above takes care of *new* writes; this catches anything the
# script created with a tighter mode (e.g. cedapug_maps writes its
# .cedapug/manifest.tsv as 0600 by default).
find "$OVERLAY_DIR" -type f ! -perm -o+r -exec chmod o+r {} + 2>/dev/null || true
find "$OVERLAY_DIR" -type d ! -perm -o+rx -exec chmod o+rx {} + 2>/dev/null || true
exit $SCRIPT_RC

View file

@ -394,26 +394,27 @@ def test_overlay_helper_is_python_with_strict_validation():
assert '"unmount"' not in text
def test_overlay_helper_idmaps_sandbox_owned_lowerdirs():
"""Script-built overlay lowerdirs are owned by l4d2-sandbox. Without an
idmap bind mount, kernel-overlayfs copy-up preserves that ownership and
the gameserver (uid left4me) can't write to copied-up directories like
addons/sourcemod/logs/. The helper must inject an idmap bind for each
sandbox-owned lowerdir before the overlay mount and tear it down after.
def test_script_sandbox_uses_idmap_staging():
"""The sandbox runs as l4d2-sandbox but writes need to land on disk as
left4me, so all overlay content (workshop + script-built) is uniformly
left4me-owned. The helper pre-creates an idmapped bind on a staging
path and points the sandbox's BindPaths at the staging, not at the raw
overlay dir. trap cleans up the staging bind on exit.
"""
text = OVERLAY_HELPER.read_text()
# The bind-mount argv uses --map-users / --map-groups (numeric uids).
text = SCRIPT_SANDBOX_HELPER.read_text()
# Idmap mount setup uses --map-users / --map-groups.
assert "--map-users=" in text
assert "--map-groups=" in text
# Idmapped paths live under runtime/<name>/idmap/ and are substituted
# into the lowerdir= string.
assert 'runtime_name_dir / "idmap"' in text
# Test-mode uid stubs are namespaced LEFT4ME_TEST_* and gated on
# PRINT_ONLY=1 so a misconfigured systemd unit can't inject uids.
assert "LEFT4ME_TEST_SANDBOX_UID" in text
assert "LEFT4ME_TEST_LEFT4ME_UID" in text
# Collision guard: two lowerdirs deriving the same idmap target die loudly.
assert "seen_idmap_targets" in text
# Staging path lives under /var/lib/left4me/tmp/sandbox-idmap-<id>.
assert "/var/lib/left4me/tmp/sandbox-idmap-" in text
# BindPaths into the sandbox points at the staging path, not the
# raw overlay dir.
assert 'BindPaths="${STAGING}:/overlay"' in text
# trap registers cleanup so the staging bind doesn't outlive the helper.
assert "trap " in text and "cleanup_staging" in text
# The previous chown-to-l4d2-sandbox approach is gone; overlay dirs
# stay left4me-owned end-to-end.
assert "chown -R l4d2-sandbox" not in text
def test_deploy_script_installs_overlay_helper_with_executable_mode():
@ -659,7 +660,7 @@ def test_script_sandbox_helper_invokes_systemd_run_with_hardening():
assert "/etc/nsswitch.conf" in text
assert "/etc/alternatives" in text
assert "${SCRIPT}:/script.sh" in text
assert 'BindPaths="${OVERLAY_DIR}:/overlay"' in text
assert 'BindPaths="${STAGING}:/overlay"' in text
# IP egress filter: allow public, deny localhost / RFC1918 / link-local /
# multicast / CGNAT / ULA. systemd's "more specific rule wins" semantics

View file

@ -41,11 +41,6 @@ def _run(args: list[str], root: Path, extra_env: dict[str, str] | None = None) -
**os.environ,
"LEFT4ME_ROOT": str(root),
"LEFT4ME_OVERLAY_PRINT_ONLY": "1",
# Inject synthetic user ids so tests work without real system users.
"LEFT4ME_TEST_SANDBOX_UID": str(FAKE_SANDBOX_UID),
"LEFT4ME_TEST_SANDBOX_GID": str(FAKE_SANDBOX_GID),
"LEFT4ME_TEST_LEFT4ME_UID": str(FAKE_LEFT4ME_UID),
"LEFT4ME_TEST_LEFT4ME_GID": str(FAKE_LEFT4ME_GID),
}
if extra_env:
env.update(extra_env)
@ -161,136 +156,6 @@ def test_rejects_empty_lowerdir_entry(tmp_path: Path) -> None:
assert "empty entry" in result.stderr
FAKE_SANDBOX_UID = 7001
FAKE_SANDBOX_GID = 7001
FAKE_LEFT4ME_UID = 7002
FAKE_LEFT4ME_GID = 7002
def _setup_instance_with_uid(
root: Path,
name: str = "alpha",
lowerdir_uid: int = FAKE_LEFT4ME_UID,
lowerdir_gid: int = FAKE_LEFT4ME_GID,
) -> Path:
"""Like _setup_instance but chowns the lowerdir to a specific uid/gid."""
overlay_dir = root / "overlays" / "workshop"
overlay_dir.mkdir(parents=True, exist_ok=True)
try:
os.chown(overlay_dir, lowerdir_uid, lowerdir_gid)
except PermissionError:
pass # tests not running as root — uid won't match; that's fine for the "skips idmap" test
(root / "installation").mkdir(parents=True, exist_ok=True)
lowerdirs = [str(overlay_dir), str(root / "installation")]
inst_dir = root / "instances" / name
inst_dir.mkdir(parents=True, exist_ok=True)
(inst_dir / "instance.env").write_text(
f"L4D2_LOWERDIRS={':'.join(lowerdirs)}\n"
)
runtime = root / "runtime" / name
(runtime / "upper").mkdir(parents=True, exist_ok=True)
(runtime / "work").mkdir(parents=True, exist_ok=True)
(runtime / "merged").mkdir(parents=True, exist_ok=True)
return overlay_dir
def test_mount_idmaps_sandbox_owned_lowerdir(tmp_path: Path) -> None:
"""A lowerdir owned by l4d2-sandbox uid triggers an idmap bind mount.
The overlay lowerdir= string must reference the idmap path, not the raw
overlay path. A mount --bind --map-users/--map-groups argv must be emitted
before the overlay mount argv.
"""
overlay_dir = _setup_instance_with_uid(
tmp_path, lowerdir_uid=FAKE_SANDBOX_UID, lowerdir_gid=FAKE_SANDBOX_GID
)
try:
os.chown(overlay_dir, FAKE_SANDBOX_UID, FAKE_SANDBOX_GID)
except PermissionError:
pytest.skip("chown requires root — skip on unprivileged runner")
result = _run(["mount", "alpha"], tmp_path)
assert result.returncode == 0, result.stderr
lines = [l for l in result.stdout.splitlines() if l.strip()]
assert len(lines) == 2, f"expected 2 argv lines, got: {result.stdout!r}"
bind_parts = shlex.split(lines[0])
assert bind_parts[0] == "/bin/mount"
assert "--bind" in bind_parts
assert f"--map-users={FAKE_SANDBOX_UID}:{FAKE_LEFT4ME_UID}:1" in bind_parts
assert f"--map-groups={FAKE_SANDBOX_GID}:{FAKE_LEFT4ME_GID}:1" in bind_parts
assert bind_parts[-2] == str(overlay_dir)
idmap_target = str(tmp_path / "runtime" / "alpha" / "idmap" / "overlays_workshop")
assert bind_parts[-1] == idmap_target
overlay_parts = shlex.split(lines[1])
assert overlay_parts[0] == "/bin/mount"
assert overlay_parts[1:3] == ["-t", "overlay"]
options = overlay_parts[5]
assert f"lowerdir={idmap_target}:" in options, \
f"lowerdir should start with idmap path; got: {options!r}"
assert str(overlay_dir) not in options, \
f"raw overlay path should not appear in lowerdir; got: {options!r}"
def test_mount_skips_idmap_for_left4me_owned_lowerdir(tmp_path: Path) -> None:
"""A lowerdir already owned by the left4me uid needs no idmap bind mount."""
overlay_dir = _setup_instance_with_uid(
tmp_path, lowerdir_uid=FAKE_LEFT4ME_UID, lowerdir_gid=FAKE_LEFT4ME_GID
)
# Best-effort chown to the left4me uid — skip if not root.
try:
os.chown(overlay_dir, FAKE_LEFT4ME_UID, FAKE_LEFT4ME_GID)
except PermissionError:
# Without root, st_uid is 0 or our own uid; neither matches FAKE_SANDBOX_UID,
# so the helper will correctly skip the idmap bind either way.
pass
result = _run(["mount", "alpha"], tmp_path)
assert result.returncode == 0, result.stderr
lines = [l for l in result.stdout.splitlines() if l.strip()]
assert len(lines) == 1, f"expected 1 argv line (no bind mount), got: {result.stdout!r}"
overlay_parts = shlex.split(lines[0])
assert overlay_parts[0] == "/bin/mount"
assert "--bind" not in overlay_parts
options = overlay_parts[5]
idmap_subdir = str(tmp_path / "runtime" / "alpha" / "idmap")
assert idmap_subdir not in options, f"idmap path should not appear; got: {options!r}"
assert str(overlay_dir) in options
def test_umount_unwinds_idmap_binds(tmp_path: Path) -> None:
"""umount emits bind-umount lines for each idmap subdir, after the overlay umount."""
_setup_instance(tmp_path)
# Pre-seed an idmap subdir as if a previous mount had set it up.
idmap_dir = tmp_path / "runtime" / "alpha" / "idmap"
idmap_dir.mkdir(parents=True)
idmap_sub = idmap_dir / "workshop"
idmap_sub.mkdir()
result = _run(["umount", "alpha"], tmp_path)
assert result.returncode == 0, result.stderr
lines = [l for l in result.stdout.splitlines() if l.strip()]
assert len(lines) >= 2, f"expected at least 2 argv lines, got: {result.stdout!r}"
# First line: overlay umount
overlay_umount_parts = shlex.split(lines[0])
assert overlay_umount_parts == [
"/bin/umount",
str(tmp_path / "runtime" / "alpha" / "merged"),
]
# Subsequent lines: bind umounts for each idmap subdir
bind_umount_parts = shlex.split(lines[1])
assert bind_umount_parts[0] == "/bin/umount"
assert bind_umount_parts[-1] == str(idmap_sub)
@pytest.mark.skipif(sys.platform != "linux", reason="user.* xattrs are Linux-only")
def test_rejects_upperdir_with_fuseoverlayfs_xattr(tmp_path: Path) -> None:
_setup_instance(tmp_path)
@ -303,47 +168,3 @@ def test_rejects_upperdir_with_fuseoverlayfs_xattr(tmp_path: Path) -> None:
result = _run(["mount", "alpha"], tmp_path)
assert result.returncode != 0
assert "fuse-overlayfs xattr" in result.stderr
def _load_helper_module():
"""Import the helper script as a Python module for unit testing internals.
The helper file has no .py extension, so importlib needs an explicit
SourceFileLoader rather than auto-detection.
"""
import importlib.util
from importlib.machinery import SourceFileLoader
loader = SourceFileLoader("left4me_overlay", str(HELPER_SOURCE))
spec = importlib.util.spec_from_loader("left4me_overlay", loader)
assert spec is not None
module = importlib.util.module_from_spec(spec)
loader.exec_module(module)
return module
def test_is_mountpoint_detects_same_fs_bind_mount(tmp_path: Path) -> None:
"""_is_mountpoint reads /proc/self/mountinfo so it works for same-fs bind mounts.
Regression: os.path.ismount() compares st_dev against the parent, which
silently returns False for same-fs bind mounts. The idmap binds we install
on runtime/<n>/idmap/<basename> are exactly that case, so an ismount-based
check skipped umount on stop and re-bound on top on start accumulating
mount-table entries across stop/start cycles.
"""
helper = _load_helper_module()
target = tmp_path / "some-bind"
target.mkdir()
abs_target = str(target.resolve())
mountinfo = tmp_path / "fake-mountinfo"
# mountinfo column 5 is the mountpoint; build a minimal line that exercises
# the parse without depending on the rest of the format.
mountinfo.write_text(
f"42 1 0:30 / {abs_target} rw,relatime - tmpfs tmpfs rw\n"
f"43 1 0:31 / /some/other/path rw,relatime - tmpfs tmpfs rw\n"
)
assert helper._is_mountpoint(target, str(mountinfo)) is True
assert helper._is_mountpoint(tmp_path / "not-a-mount", str(mountinfo)) is False
assert helper._is_mountpoint(target, str(tmp_path / "no-such-file")) is False