diff --git a/doc/run.md b/doc/run.md index a984dd70c..b48675122 100644 --- a/doc/run.md +++ b/doc/run.md @@ -667,8 +667,8 @@ mount -t virtiofs microvm /mnt/host ## Experimental single-workload sandbox -The `sandbox` command launches the microVM with one to three compressed -EROFS lower layers and one preformatted ext4 scratch image: +The `sandbox` command launches the microVM with one compressed EROFS `distro` +layer and one preformatted ext4 scratch image: ```bash python3 scripts/nvx.py build-distro-layer \ diff --git a/doc/usage.md b/doc/usage.md index 142489838..135e1e72a 100644 --- a/doc/usage.md +++ b/doc/usage.md @@ -707,7 +707,7 @@ launches. | Option | Default | Description | | --- | --- | --- | | `{run,provision,start,exec,stop,deprovision}` | `run` | Select a one-shot run or a managed lifecycle operation. | -| `--layer ROLE,PATH,EROFS_UUID` | required for `run` and `provision` | Attach a `distro`, `runtime`, or `custom` EROFS layer. Repeat once per distinct role. | +| `--layer ROLE,PATH,EROFS_UUID` | required for `run` and `provision` | Attach the `distro` EROFS layer. It is the only supported role, and it may be given once. | | `--scratch PATH` | required for `run` and `provision` | Attach a preformatted ext4 scratch image as the writable overlay. | | `--state-dir PATH` | required for managed operations | Select persistent sandbox state. One-shot `run` rejects this option. | | `--entrypoint PATH` | `/bin/sh` | Select an absolute workload entrypoint without whitespace. | diff --git a/guest/common/nvx-init-agent b/guest/common/nvx-init-agent index 56f4a8527..12bfbb95e 100644 --- a/guest/common/nvx-init-agent +++ b/guest/common/nvx-init-agent @@ -396,7 +396,7 @@ teardown() { echo "NVX-SANDBOX-ERROR: failed to unmount the container overlay" >&2 status=1 fi - for role in custom runtime distro; do + for role in distro; do directory=$layers/$role if mountpoint -q "$directory" && ! umount "$directory"; then echo "NVX-SANDBOX-ERROR: failed to unmount the $role layer" >&2 @@ -423,8 +423,6 @@ configure_cgroups layer_capacity=$(cmdline_value nvx_layer_capacity_sectors || true) distro=false -runtime_layer=false -custom=false layer_count=0 # Kernel command-line tokens are deliberately split on spaces. # shellcheck disable=SC2086 @@ -446,18 +444,10 @@ for token in $cmdline; do [ "$distro" = false ] || fatal "duplicate distro layer" distro=true ;; - runtime) - [ "$runtime_layer" = false ] || fatal "duplicate runtime layer" - runtime_layer=true - ;; - custom) - [ "$custom" = false ] || fatal "duplicate custom layer" - custom=true - ;; *) fatal "unknown layer role: $role" ;; esac layer_count=$((layer_count + 1)) - [ "$layer_count" -le 3 ] || fatal "at most three layers are supported" + [ "$layer_count" -le 1 ] || fatal "exactly one layer is supported" set +f mount_layer "$role" "$address" "$expected_uuid" "$layer_capacity" set -f @@ -471,12 +461,6 @@ lowerdirs= if [ "$distro" = true ]; then lowerdirs=$layers/distro fi -if [ "$runtime_layer" = true ]; then - lowerdirs=$layers/runtime${lowerdirs:+:$lowerdirs} -fi -if [ "$custom" = true ]; then - lowerdirs=$layers/custom${lowerdirs:+:$lowerdirs} -fi scratch_spec=$(cmdline_value nvx_scratch || true) [ -n "$scratch_spec" ] || fatal "a scratch device is required" diff --git a/scripts/nvx_tools/microvm_test_scripts/sandbox-blocks.sh b/scripts/nvx_tools/microvm_test_scripts/sandbox-blocks.sh index 2ce10bc28..80e72c66b 100644 --- a/scripts/nvx_tools/microvm_test_scripts/sandbox-blocks.sh +++ b/scripts/nvx_tools/microvm_test_scripts/sandbox-blocks.sh @@ -6,7 +6,7 @@ fail() { exit "$code" } -for name in vda vdb vdc vdd; do +for name in vda vdb; do tries=0 while [ ! -b "/dev/$name" ] && [ "$tries" -lt 200 ]; do sleep 0.05 @@ -15,14 +15,12 @@ for name in vda vdb vdc vdd; do [ -b "/dev/$name" ] || fail 20 done grep -q 'virtio_mmio.device=0x1000@0xd0003000:4' /proc/cmdline || fail 21 -grep -q 'virtio_mmio.device=0x1000@0xd0004000:12' /proc/cmdline || fail 22 -grep -q 'virtio_mmio.device=0x1000@0xd0005000:9' /proc/cmdline || fail 23 -grep -q 'virtio_mmio.device=0x1000@0xd0006000:11' /proc/cmdline || fail 24 +grep -q 'virtio_mmio.device=0x1000@0xd0006000:11' /proc/cmdline || fail 22 +if grep -q 'virtio_mmio.device=0x1000@0xd0004000:' /proc/cmdline; then fail 23; fi +if grep -q 'virtio_mmio.device=0x1000@0xd0005000:' /proc/cmdline; then fail 24; fi [ "$(cat /sys/block/vda/ro)" = 1 ] || fail 25 -[ "$(cat /sys/block/vdb/ro)" = 1 ] || fail 26 -[ "$(cat /sys/block/vdc/ro)" = 1 ] || fail 27 -[ "$(cat /sys/block/vdd/ro)" = 0 ] || fail 28 -printf MICROVM-SCRATCH-OK | dd of=/dev/vdd bs=512 count=1 conv=sync,notrunc 2>/dev/null -[ "$(dd if=/dev/vdd bs=512 count=1 2>/dev/null | head -c 18)" = MICROVM-SCRATCH-OK ] || fail 29 +[ "$(cat /sys/block/vdb/ro)" = 0 ] || fail 26 +printf MICROVM-SCRATCH-OK | dd of=/dev/vdb bs=512 count=1 conv=sync,notrunc 2>/dev/null +[ "$(dd if=/dev/vdb bs=512 count=1 2>/dev/null | head -c 18)" = MICROVM-SCRATCH-OK ] || fail 29 echo NVX-SANDBOX-BLOCKS-OK nvx-exit 0 diff --git a/scripts/nvx_tools/microvm_tests.py b/scripts/nvx_tools/microvm_tests.py index 0138450b8..18b76550b 100644 --- a/scripts/nvx_tools/microvm_tests.py +++ b/scripts/nvx_tools/microvm_tests.py @@ -4614,7 +4614,7 @@ def run_sandbox_blocks( memory_mib, "quiet loglevel=0", ) - for role in ("distro", "runtime", "custom", "scratch"): + for role in ("distro", "scratch"): path = root / f"{role}.img" with path.open("wb") as disk: disk.truncate(SANDBOX_BLOCK_SIZE) diff --git a/scripts/nvx_tools/sandbox.py b/scripts/nvx_tools/sandbox.py index 4a4307524..68a18652b 100644 --- a/scripts/nvx_tools/sandbox.py +++ b/scripts/nvx_tools/sandbox.py @@ -12,11 +12,10 @@ from .common import ScriptError, require_file -LAYER_ROLES = ("distro", "runtime", "custom") +# Only the distro layer is produced and attached. OpenVMM reserves the other MMIO windows. +LAYER_ROLES = ("distro",) BLOCK_MMIO_BASES = { "distro": 0xD000_3000, - "runtime": 0xD000_4000, - "custom": 0xD000_5000, "scratch": 0xD000_6000, } KERNEL_COMMAND_LINE_MAX_SIZE = 2048 @@ -520,14 +519,14 @@ class SandboxLaunch: mounts: tuple[SandboxMount, ...] = () def __post_init__(self) -> None: - if not 1 <= len(self.layers) <= len(LAYER_ROLES): - raise ScriptError("a sandbox requires one to three read-only layers") - roles = [layer.role for layer in self.layers] - duplicates = sorted({role for role in roles if roles.count(role) > 1}) - if duplicates: - raise ScriptError( - f"duplicate sandbox layer role(s): {', '.join(duplicates)}" - ) + if len(self.layers) != 1: + raise ScriptError("a sandbox requires exactly one read-only layer: distro") + for layer in self.layers: + if layer.role not in LAYER_ROLES: + raise ScriptError( + f"unsupported layer role {layer.role!r}; " + f"choose {', '.join(LAYER_ROLES)}" + ) if not self.entrypoint.startswith("/") or any( character.isspace() for character in self.entrypoint ): diff --git a/scripts/test_microvm_tests.py b/scripts/test_microvm_tests.py index fc2976302..cadad1fed 100644 --- a/scripts/test_microvm_tests.py +++ b/scripts/test_microvm_tests.py @@ -7588,13 +7588,13 @@ def test_sandbox_blocks_use_fixed_roles_and_access(self): for index, value in enumerate(command) if value == "--microvm-sandbox-block" ] - self.assertEqual(len(values), 4) + # NVX attaches only the distro layer and scratch; the freed runtime and custom + # windows stay unused. + self.assertEqual(len(values), 2) self.assertTrue(values[0].startswith("distro:file:")) - self.assertTrue(values[1].startswith("runtime:file:")) - self.assertTrue(values[2].startswith("custom:file:")) - self.assertTrue(values[3].startswith("scratch:file:")) - self.assertTrue(all(value.endswith(",ro") for value in values[:3])) - self.assertFalse(values[3].endswith(",ro")) + self.assertTrue(values[0].endswith(",ro")) + self.assertTrue(values[1].startswith("scratch:file:")) + self.assertFalse(values[1].endswith(",ro")) self.assertEqual( run_guest_script.call_args.args[2], microvm_tests.SANDBOX_BLOCKS_COMPLETION_MARKER, diff --git a/scripts/test_nvx_tools.py b/scripts/test_nvx_tools.py index 2f4c59ac5..feab5c9c2 100644 --- a/scripts/test_nvx_tools.py +++ b/scripts/test_nvx_tools.py @@ -12099,15 +12099,12 @@ def test_check_fails_when_a_rejection_reports_another_cause(self): class SandboxTests(unittest.TestCase): - def test_launch_contract_orders_roles_and_builds_agent_command_line(self): - custom = sandbox.SandboxLayer.parse( - "custom,custom.erofs,22222222-2222-2222-2222-222222222222" - ) + def test_launch_contract_builds_agent_command_line(self): distro = sandbox.SandboxLayer.parse( "distro,distro.erofs,11111111-1111-1111-1111-111111111111" ) launch = sandbox.SandboxLaunch( - layers=(custom, distro), + layers=(distro,), scratch=Path("scratch.ext4"), entrypoint="/bin/workload", args=("--serve",), @@ -12117,16 +12114,11 @@ def test_launch_contract_orders_roles_and_builds_agent_command_line(self): pids_max=64, ) - self.assertEqual( - [layer.role for layer in launch.ordered_layers()], - ["distro", "custom"], - ) self.assertEqual( launch.kernel_command_line("quiet"), ( "quiet nvx_sandbox=1 " "nvx_layer=distro,0xd0003000,11111111-1111-1111-1111-111111111111 " - "nvx_layer=custom,0xd0005000,22222222-2222-2222-2222-222222222222 " "nvx_scratch=0xd0006000,ext4 " "nvx_entrypoint=/bin/workload nvx_hostname=example " "nvx_arg=--serve nvx_memory_max=268435456 nvx_pids_max=65" @@ -12140,8 +12132,6 @@ def test_launch_contract_orders_roles_and_builds_agent_command_line(self): "--microvm-sandbox-block", "distro:file:distro.erofs,ro", "--microvm-sandbox-block", - "custom:file:custom.erofs,ro", - "--microvm-sandbox-block", "scratch:file:scratch.ext4", "--microvm-workload-identity", "1000:1001", @@ -12152,7 +12142,7 @@ def test_launch_contract_rejects_duplicates_and_reserved_tokens(self): distro = sandbox.SandboxLayer.parse( "distro,distro.erofs,11111111-1111-1111-1111-111111111111" ) - with self.assertRaisesRegex(common.ScriptError, "duplicate.*distro"): + with self.assertRaisesRegex(common.ScriptError, "exactly one"): sandbox.SandboxLaunch( layers=(distro, distro), scratch=Path("scratch.ext4"), @@ -12171,6 +12161,62 @@ def test_launch_contract_rejects_duplicates_and_reserved_tokens(self): with self.assertRaisesRegex(common.ScriptError, "between 1"): sandbox.parse_workload_identity("0:0") + def test_layer_parser_rejects_the_runtime_and_custom_roles(self): + # Nothing produces or attaches these layers, so NVX accepts only distro. + for role in ("runtime", "custom"): + with ( + self.subTest(role=role), + self.assertRaisesRegex(common.ScriptError, "unsupported layer role"), + ): + sandbox.SandboxLayer.parse( + f"{role},layer.erofs,11111111-1111-1111-1111-111111111111" + ) + + def test_launch_rejects_unsupported_layer_roles_built_directly(self): + # Constructing SandboxLaunch directly bypasses parse(); the role must + # still be rejected, or ordered_layers() silently drops the layer. + for role in ("runtime", "custom"): + with ( + self.subTest(role=role), + self.assertRaisesRegex(common.ScriptError, "unsupported layer role"), + ): + sandbox.SandboxLaunch( + layers=( + sandbox.SandboxLayer( + role=role, + path=Path("layer.erofs"), + uuid="11111111-1111-1111-1111-111111111111", + ), + ), + scratch=Path("scratch.ext4"), + ) + + def test_managed_deserialize_rejects_unsupported_layer_role(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + layer_path = root / "runtime.erofs" + scratch_path = root / "scratch.ext4" + layer_path.write_bytes(b"layer") + scratch_path.write_bytes(b"scratch") + config = { + "format": sandbox_lifecycle._config_format(()), + "layers": [ + { + "role": "runtime", + "path": os.fspath(layer_path), + "uuid": "11111111-1111-1111-1111-111111111111", + } + ], + "scratch": os.fspath(scratch_path), + "hostname": "nvx-sandbox", + "workload_uid": 1000, + "workload_gid": 1000, + "memory_max": None, + "pids_max": None, + } + with self.assertRaisesRegex(common.ScriptError, "unsupported layer role"): + sandbox_lifecycle._deserialize_launch(config) + def test_layer_parser_rejects_invalid_role_and_uuid(self): with self.assertRaisesRegex(common.ScriptError, "unsupported layer role"): sandbox.SandboxLayer.parse(