From 3904661440d8fb5ca2e2274a2b4a38320ad83e2e Mon Sep 17 00:00:00 2001 From: Paul Meyer Date: Wed, 26 Aug 2026 11:33:34 +0200 Subject: [PATCH] test: make boot mode work in unprivileged containers In a mount namespace owned by a non-initial user namespace, the kernel only allows mounting a fresh proc or sysfs if the namespace already contains a fully visible mount of the same filesystem type (mount_too_revealing()). systemd-nspawn's pivoted root contains no such mount, so booting the test container inside an unprivileged container (e.g. a containerized CI runner) fails with EPERM when mounting /proc. When running in a non-initial user namespace, satisfy the check by binding a read-only instance of each into the container under /work. Skipping the binds on regular privileged runs keeps the container isolated from the host's proc and sysfs. Binding non-recursively keeps foreign submounts (e.g. lxcfs overmounts) out, which would spoil full visibility where they end up locked, and which read-only would not cover. Signed-off-by: Paul Meyer --- .../integration-test-wrapper.py | 30 ++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/test/integration-tests/integration-test-wrapper.py b/test/integration-tests/integration-test-wrapper.py index ed8a8f2607..19dcf2bc28 100755 --- a/test/integration-tests/integration-test-wrapper.py +++ b/test/integration-tests/integration-test-wrapper.py @@ -365,6 +365,15 @@ def process_coverage(args: argparse.Namespace, summary: Summary, name: str, jour print(f'Wrote coverage report for {name} to {output}', file=sys.stderr) +def in_userns() -> bool: + # The initial user namespace has a well-known inode number (USER_NS_INIT_INO, linux/nsfs.h). + # Without kernel support for user namespaces, ours can only be the initial one. + try: + return os.stat('/proc/self/ns/user').st_ino != 0xEFFFFFFD + except OSError: + return False + + def coco_host_type() -> Optional[str]: try: if Path('/sys/module/kvm_intel/parameters/tdx').read_text().strip() == 'Y': @@ -706,7 +715,26 @@ def main() -> None: *(['--runtime-build-sources=no', '--register=no'] if not sys.stdin.isatty() else []), 'vm' if vm else 'boot', *( - ['--', '--capability=CAP_BPF', f'--suppress-sync={"yes" if args.suppress_sync else "no"}', *vm_images_args, *coco_boot_args] + [ + '--', + '--capability=CAP_BPF', + f'--suppress-sync={"yes" if args.suppress_sync else "no"}', + # In a non-initial user namespace the kernel only allows mounting a fresh proc or + # sysfs if the mount namespace already contains a fully visible mount of the same + # filesystem type (mount_too_revealing()), which nspawn's pivoted root doesn't. + # Satisfy the check with read-only binds, but only when actually confined, as + # they expose the outer proc and sysfs to the container. Bind non-recursively: + # carried-along submounts (e.g. lxcfs overmounts of container runtimes) would + # spoil full visibility wherever they end up locked, and --bind-ro wouldn't make + # them read-only either. + *( + ['--bind-ro=/proc:/work/proc:norbind', '--bind-ro=/sys:/work/sys:norbind'] + if in_userns() + else [] + ), + *vm_images_args, + *coco_boot_args, + ] if not vm else [] ),