diff --git a/test/integration-tests/README.md b/test/integration-tests/README.md index 229e802632..e5f7c76713 100644 --- a/test/integration-tests/README.md +++ b/test/integration-tests/README.md @@ -120,6 +120,15 @@ rerun the test. `TEST_NO_KVM=1`: Disable qemu KVM auto-detection (may be necessary when you're trying to run the *vanilla* qemu and have both qemu and qemu-kvm installed) +`TEST_MKOSI_ARGS=options`: Extra options (split like a shell command line) +passed to the `mkosi` invocation that launches the test image, without +affecting the image build. Only the `--option=value` syntax is accepted. For example, +`TEST_MKOSI_ARGS="--format=directory --output-dir=build/mkosi.output.dir"` +boots the test container from a directory format output (which has to be built +beforehand, into a separate output directory), allowing boot mode tests to run +in environments that can't dissect disk images, such as unprivileged +containers, where loop devices and udev are not available. + `TEST_MATCH_SUBTEST=subtest`: If the test makes use of `run_subtests` use this variable to provide a POSIX extended regex to run only subtests matching the expression. diff --git a/test/integration-tests/integration-test-wrapper.py b/test/integration-tests/integration-test-wrapper.py index ed8a8f2607..1324b6ccd3 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': @@ -661,6 +670,12 @@ def main() -> None: ''' ) + # Extra mkosi options for this invocation only. Each has to use the '--option=value' syntax. + test_mkosi_args = shlex.split(os.getenv('TEST_MKOSI_ARGS', '')) + for arg in test_mkosi_args: + if not re.fullmatch('--[^=]+=.*', arg): + sys.exit(f"TEST_MKOSI_ARGS must contain only '--option=value' mkosi options, got: {arg}") + cmd = [ args.mkosi, '--directory', os.fspath(args.mkosi_dir), @@ -704,9 +719,29 @@ def main() -> None: ), '--credential', f"journal.storage={'persistent' if sys.stdin.isatty() else args.storage}", *(['--runtime-build-sources=no', '--register=no'] if not sys.stdin.isatty() else []), + *test_mkosi_args, '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 [] ),